线程安全问题及解决办法

JDK5.0之前就已经有了两种解决线程安全的问题:

  • 同步代码块
  • 同步方法

JDK5.0后新增了一种解决方案:

  • 同步锁

同步的方法虽然能解决线程安全问题,但它带来的坏处就是降低了效率。所以,我们应该尽量减少需要同步的代码块,必不可少的地方才去用同步方法。

我们首先分别用继承Thread以及实现Runnable接口的方法,写出一个含线程安全的程序。

模拟业务:多窗口卖票(使用多线程模拟多个窗口),共有篇数100张。

继承Thread类方案:

// 继承Thread类
class Window1 extends Thread
{
    private static int ticket = 100;

    @Override
    public void run ()
    {
        while (true) {
            if (ticket > 0) {
                // 模拟卖票业务,每次卖票耗时0.1s
                try {
                    sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println(getName() + ":票号" + ticket--);
            } else {
                break;
            }
        }
    }
}

public class Window1Test {
    public static void main(String[] args) {
        Window1 window11 = new Window1();
        Window1 window12 = new Window1();
        Window1 window13 = new Window1();

        window11.setName("窗口1");
        window12.setName("窗口2");
        window13.setName("窗口3");

        window11.start();
        window12.start();
        window13.start();
    }
}

实现Runnable接口的方法

// 实现Runnable接口方法
class Window2 implements Runnable
{
    private static int ticket = 100;

    @Override
    public void run() {
        while (true) {
            if (ticket > 0) {
                // 模拟卖票业务,每次卖票耗时0.1s
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println(Thread.currentThread().getName() + ":票号" + ticket--);
            } else {
                break;
            }
        }
    }
}

public class Window2Test {
    public static void main(String[] args) {
        Window2 window2 = new Window2();

        Thread thread1 = new Thread(window2);
        Thread thread2 = new Thread(window2);
        Thread thread3 = new Thread(window2);

        thread1.setName("窗口1");
        thread2.setName("窗口2");
        thread3.setName("窗口3");

        thread1.start();
        thread2.start();
        thread3.start();
    }
}

同步代码块

语法:

 synchronized (同步监视器) {
   // 需要同步的代码         
 }

同步监视器(锁)可以是任何类型的对象,但是多个线程要使用同一个同步监视器,否则依旧不是线程安全的。

我们首先用同步代码块的方式来改进上述的继承Thread版卖票程序:

public void run ()
{
    while (true) {
        synchronized (Window1.class) {
            if (ticket > 0) {
                // 模拟卖票业务,每次卖票耗时0.1s
                try {
                    sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println(getName() + ":票号" + ticket--);
            } else {
                break;
            }
        }
    }
}

修改实现Runnable接口的卖票程序方法和上述一样

public void run() {
    while (true) {
        synchronized (Window2.class) {
            if (ticket > 0) {
                // 模拟卖票业务,每次卖票耗时0.1s
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println(Thread.currentThread().getName() + ":票号" + ticket--);
            } else {
                break;
            }
        }

    }
}

同步方法

如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。

注意:同步方法虽然不能手动设置同步监视器,但它确实是存在的。实例方法的同步监视器为this(不一定唯一),而静态方法的同步监视器为当前类.class(唯一)

使用同步方法修改Thread版卖票程序

// 继承Thread类
class Window1 extends Thread
{
    private static int ticket = 100;

    @Override
    public void run ()
    {
        while (ticket > 0) {
            ticketSale();
        }
    }

    private static synchronized void ticketSale ()
    {
        if (ticket > 0) {
            // 模拟卖票业务,每次卖票耗时0.1s
            try {
                sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(Thread.currentThread().getName() + ":票号" + ticket--);
        }
    }
}

使用同步方法修改Runnable版卖票程序

// 实现Runnable接口方法
class Window2 implements Runnable
{
    private static int ticket = 100;

    @Override
    public void run() {
        while (ticket > 0) {
           ticketSale();
        }
    }

    private synchronized void ticketSale ()
    {
        if (ticket > 0) {
            // 模拟卖票业务,每次卖票耗时0.1s
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(Thread.currentThread().getName() + ":票号" + ticket--);
        }
    }
}

同步锁

从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。

  • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
  • ReentrantLock类实现了Lock,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
class Window3 implements Callable
{
    private static int ticket = 100;
    private static final ReentrantLock lock = new ReentrantLock();

    @Override
    public Object call()  {
        while (true) {
            lock.lock();
            try {
                if (ticket > 0) {
                    // 模拟卖票业务,每次卖票耗时0.1s
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName() + ":票号" + ticket--);
                } else {
                    break;
                }
            } finally {
                lock.unlock();
            }

        }

        return null;
    }
}

public class Window3Test {
    public static void main(String[] args) {
        FutureTask[] tasks = new FutureTask[3];
        Callable callable = new Window3();

        for (int i = 0; i < 3; i++)
        {         
            tasks[i] = new FutureTask(callable);

            Thread t = new Thread(tasks[i]);
            t.start();
        }
    }
}

synchronized 与 Lock的异同?

相同:

  • 二者都可以解决线程安全问题

不同:

  • synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器。Lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())。
  • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有
    更好的扩展性(提供更多的子类)

在实际开发选择中,到底应该使用哪种解决方案?

  • 通常情况下,优先选择使用同步锁;其次是同步代码块;如果整个方法都是需要同步的代码,就选择同步方法。