1、概述
当多个线程运行同一个任务时,很容易出现线程安全问题。

2、举例:卖票问题,以此来揭示线程安全问题。代码如下:

public class Thread01SafeTest01 extends Thread{
        public static void main(String[] args) {
            Runnable runnable = new Ticket();               //创建一个卖票的任务叫runnable
            Thread thread1 = new Thread(runnable);          //创建第一个线程,命名卖票线程一,并启动
            thread1.setName("卖票线程一");
            thread1.start();
            Thread thread2 = new Thread(runnable);          //创建第二个线程,命名卖票线程二,并启动
            thread2.setName("卖票线程二");
            thread2.start();
            Thread thread3 = new Thread(runnable);          //创建第三个线程,命名卖票线程三,并启动
            thread3.setName("卖票线程三");
            thread3.start();
        }

        //类:卖票
        static class Ticket implements Runnable{
            //票数计数器count
            private int count =10;
            @Override
            public void run() {
                while(count>0){
                    System.out.println("正在准备买票");               //这里是卖票操作,每卖一张,就输出一次。

                    /*这里的try--catch结构,进行停留一秒,是为了留出一定的时间,以便在运行结果中看得出由于线程导致的运行结果错误。
                    假设现在count=1,,三个线程经过了判断,都处在 while(count>0)和 count--; 这两句之间运行时,当其中一个进行了
                     count--,紧接着第二个,第三个会依次进行 count--;就会出现输出count=-1,count=-2这样的情况,而按我们的现实
                    中的情况而言,很明显,当票没有了,怎么可能还有票卖呢,最后面买票的人岂不是没有座位可以坐,因为票卖超了。
                    所以解决办法是:当有线程在卖票的时候,另外一个线程必须要等着前面的线程卖完了一张,才能接着进入卖票的代码块,
                    我们可以通过Synchronized来控制这个过程。*/
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    count--;                                        //卖票操作完成后,票数减一
                    //告知哪个线程卖的票,和余票数量
                    System.out.println(Thread.currentThread().getName()+"出票成功,余票:"+count);
                }
            }
        }
    }

运行结果:

java多线程抛出异常 java多线程报错_开发语言

3个线程共同卖票,结果票卖超了。必须对线程加以控制,所存在的问题在代码中已有注释。

3、解决办法
3.1同步代码块(代码和上述代码基本一样,主要看“=”行标示

public class Thread02SafeTest02Synchronized extends Thread {

    /**
     * 主要主题:线程同步:synchronized
     * 一、线程不安全
     * 解决方案一:同步代码块(即需要排队执行的代码块)
     * 格式:synchronized(被锁的对象){
     *                    同步代码块
     *                   }
     * @param args
     */
        //下面为主程序main
        public static void main(String[] args) {
            Runnable runnable2= new Ticket();               //创建一个卖票的任务叫runnable
            Thread thread1 = new Thread(runnable2);          //创建第一个线程,命名卖票线程一,并启动
            thread1.setName("卖票线程一");
            Thread thread2 = new Thread(runnable2);          //创建第二个线程,命名卖票线程二,并启动
            thread2.setName("卖票线程二");
            Thread thread3 = new Thread(runnable2);          //创建第三个线程,命名卖票线程三,并启动
            thread3.setName("卖票线程三");
            thread1.start();
            thread2.start();
            thread3.start();
        }

        //类:卖票
        static class Ticket implements Runnable{
            //票数计数器count
            private int count =20;
            //用来被锁的对象,需要创建run()方法之前,因为作为被锁的对象只能是一个,才能实现多线程排队执行的效果。
            private Object object = new Object();

            //重写run方法
            @Override
            public void run() {
//                private Object object = new Object();如果在这里创建,那么每个线程运行时都创建了一把锁,那么就实现不了锁的控制作用了。
                    while (true) {
                        //synchronized把下面两行“=”之间的代码块,关起来了,并且还把门上了一把锁,这把锁就是下面
                        // synchronized(object)中的object,所以多个线程此时只能向上厕所一样,只有前面的线程执行完毕,
                        // 锁开了才能进去,即排队一个一个进去执行,就避免了类ThreadSafeTest01运行结果中的线程安全问题。
                        synchronized(object) {
                            //同步代码块
                            //========================================================================================
                            if (count > 0) {
                                System.out.println("正在准备买票");               //这里是卖票操作,每卖一张,就输出一次。
                                try {
                                    Thread.sleep(100);                     //停顿一下,方便看输出结果。
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }
                                count--;                                        //卖票操作完成后,票数减一
                                //告知哪个线程卖的票,和余票数量
                                System.out.println(Thread.currentThread().getName() + "出票成功,余票:" + count);
                            }else{
                                break;
                            }
                            //========================================================================================
                        }
                    }
            }
        }
}

运行结果:

java多线程抛出异常 java多线程报错_java_02

3.2 同步方法(代码和上述代码基本一样,主要看“=”行标示)

public class Thread03SafeTest03Synchronized {
    /**
     * 主要主题:线程同步:synchronized
     * 一、线程不安全
     * 解决方案二:同步方法(把需要排队执行的代码块,或者说多线程执行时会造成混乱的代码,单独提出来做成方法即可)。
     * 格式:synchronized 方法名(){
     *                    代码块
     *                   }
     *     即:用synchronized修饰方法。
     *  注意:
     *  1、在同步方法的做法下,哪个对象调用了synchronized修饰的方法,那这个对象就是那把控制多线程排队执行的锁,即synchronized锁的
     *  是当前对象,也就是this。
     *  2、如果同时存在同步代码块和同步方法,且锁相同,比如都是this,相当于两个试衣间只建了一条门,用的是同一把锁。
     * @param args
     */
    public static void main(String[] args) {
        Runnable runnable3 = new Ticket();               //创建一个卖票的任务叫runnable
        Thread thread1 = new Thread(runnable3);          //创建第一个线程,命名卖票线程一,并启动
        thread1.setName("卖票线程一");
        Thread thread2 = new Thread(runnable3);          //创建第二个线程,命名卖票线程二,并启动
        thread2.setName("卖票线程二");
        Thread thread3 = new Thread(runnable3);          //创建第三个线程,命名卖票线程三,并启动
        thread3.setName("卖票线程三");
        thread1.start();
        thread2.start();
        thread3.start();
    }

    //类:卖票
    static class Ticket implements Runnable{
        //票数计数器count
        private int count =20;

        //重写run方法
        @Override
        public void run() {
            while (true) {
                boolean flag = sale();  //这里调用了方法sale();
                if (!flag){
                    break;
                }
            }
        }


        //给方法添加synchronized
        //============================================================================================================
        public synchronized boolean sale(){
            if (count > 0) {
                System.out.println("正在准备买票");               //这里是卖票操作,每卖一张,就输出一次。
                try {
                    Thread.sleep(1000);                     //停顿一下,方便看输出结果。
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                count--;                                        //卖票操作完成后,票数减一
                //告知哪个线程卖的票,和余票数量
                System.out.println(Thread.currentThread().getName() + "出票成功,余票:" + count);
                return true;
            }else{
                return false;
            }
        }
        //============================================================================================================
    }
}

运行结果:

java多线程抛出异常 java多线程报错_java多线程抛出异常_03

3.3 显示锁Lock(代码和上述代码基本一样,主要看“=”行标示)

public class Thread04SafeTest04Lock {
    /**
     * 主要主题:线程同步:Lock
     * 一、线程不安全
     * 解决方案三:显示锁(即把需要排队执行的代码块锁起来)
     * 显示锁:Lock  子类  ReentrantLock
     * @param args
     */
    //下面是主程序
    public static void main(String[] args) {
        Runnable runnable3 = new Ticket();               //创建一个卖票的任务叫runnable
        Thread thread1 = new Thread(runnable3);          //创建第一个线程,命名卖票线程一,并启动
        thread1.setName("卖票线程一");
        Thread thread2 = new Thread(runnable3);          //创建第二个线程,命名卖票线程二,并启动
        thread2.setName("卖票线程二");
        Thread thread3 = new Thread(runnable3);          //创建第三个线程,命名卖票线程三,并启动
        thread3.setName("卖票线程三");
        thread1.start();
        thread2.start();
        thread3.start();
    }

    //类:卖票
    static class Ticket implements Runnable {
        //票数计数器count
        private int count = 20;
        //========================================================================================
        //创建一个锁
        Lock l = new ReentrantLock();
        //Lock l = new ReentrantLock(true);    //如果fair参数为true,那么显示锁就为公平锁
        //========================================================================================

        //重写run方法
        @Override
        public void run() {
            while (true) {
                //========================================================================================
                l.lock();           //在此处上锁
                //====================================下方为需要锁起来的代码块===============================
                if (count > 0) {
                    System.out.println("正在准备买票");               //这里是卖票操作,每卖一张,就输出一次。
                    try {
                        Thread.sleep(100);                     //停顿一下,方便看输出结果。
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    count--;                                        //卖票操作完成后,票数减一
                    //告知哪个线程卖的票,和余票数量
                    System.out.println(Thread.currentThread().getName() + "出票成功,余票:" + count);
                } else {
                    break;
                }
                //========================================================================================
                l.unlock();         //在此处解锁
                //========================================================================================
            }
        }
    }
}

运行结果:

java多线程抛出异常 java多线程报错_开发语言_04


4、总得来说,线程安全基本解决办法为上述三种,其本质对容易产生线程安全问题的代码块进行控制。当然还有公平锁、以及线程锁死等问题。