多线程安全(synchronized、三大特性、Java内存模型)

 2019-12-10 15:50  阅读(628)
文章分类:Java Core

线程安全问题?

什么是线程安全问题?简单的说,当多个线程在共享同一个变量,做读写的时候,会由于其他线程的干扰,导致数据误差,就会出现线程安全问题。

比如说,多个窗口同时卖票这个案例:

1 public class ThreadTrain2 implements Runnable {
     2     private int tickets = 50;
     3     @Override
     4     public void run() {
     5         while(tickets > 0){
     6                 if (tickets > 0) {                            
                  System.out.println(Thread.currentThread().getName() + "卖了第" + (50 - tickets + 1) + "张票");
     7                     tickets--;
     8                 }
     9             }
    10         }        
    11     }
    12     public static void main(String[] args) {
    13         ThreadTrain2 tt = new ThreadTrain2();
    14         Thread th1 = new Thread(tt, "1号窗口");
    15         Thread th2 = new Thread(tt, "2号窗口");
    16         th1.start();
    17         th2.start();    
    18     }
    19 }

模拟两个窗口共同卖50张票,什么都不考虑,按照上面的写法,运行的结果有时候并不是我们想要的,会完全乱了套。

我们该如何解决多线程安全问题?

使用多线程同步(synchronized)或者加锁lock

什么是多线程同步?就是当多个线程共享同一个资源时,不会受到其他线程的干扰。

为什么这两种方法可以解决线程的安全问题?

当把可能发生冲突的代码包裹在synchronized或者lock里面后,同一时刻只会有一个线程执行该段代码,其他线程必须等该线程执行完毕释放锁以后,才能去抢锁,获得锁以后,才拥有执行权,这样就解决的数据的冲突,实现了线程的安全。

卖票的案例同步后为:

1 public class ThreadTrain2 implements Runnable {
     2     private int tickets = 50;
     3     private static Object obj = new Object();//锁的对象,可以是任意的对象
     4     @Override
     5     public void run() {
     6         while(tickets > 0){ 
     7             synchronized (obj) {// 同步代码块
     8                 if (tickets > 0) {
     9                     System.out.println(Thread.currentThread().getName() + "卖了第" + (50 - tickets + 1) + "张票");
    10                     tickets--;
    11                 }
    12             }
    13         }    
    14     }
    15     public static void main(String[] args) {
    16         ThreadTrain2 tt = new ThreadTrain2();
    17         Thread th1 = new Thread(tt, "1号窗口");
    18         Thread th2 = new Thread(tt, "2号窗口");
    19         th1.start();
    20         th2.start();    
    21     }
    22 }
    23

上面是同步代码块的加锁方式,可以解决线程安全问题。同时,还有一种同步函数的方式,就是在方法上直接加synchronized,可以实现同样的效果,那么现在有一个问题,在方法上加synchronized修饰,锁的对象是什么呢???this。。下面来验证一下为什么是this:

20191210001345\_1.png
20191210001345\_2.png

public class ThreadTrain1 implements Runnable {
        private int tickets = 100;
        private static Object obj = new Object();
        private static boolean flag = true;
        @Override
        public void run() {
            if (flag) {
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                synchronized (this) {// 同步代码块
                    while (tickets > 0) {
                        if (tickets > 0) {
                            System.out.println(Thread.currentThread().getName() + "卖了第" + (100 - tickets + 1) + "张票");
                            tickets--;
                        }
                    }
                }
            } else {
                while (tickets > 0) {
                    sale();
                }
            }
        }
        public synchronized void sale() {
            if (tickets > 0) {
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
                System.out.println(Thread.currentThread().getName() + "卖了第" + (100 - tickets + 1) + "张票");
                tickets--;
            }
        }
        public static void main(String[] args) {
            ThreadTrain1 tt = new ThreadTrain1();
            Thread th1 = new Thread(tt, "1号窗口");
            Thread th2 = new Thread(tt, "2号窗口");
            th1.start();
            try {
                Thread.sleep(40);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            ThreadTrain1.flag = false;
            th2.start();
        }
    }

View Code

点击+号查看代码,代码中的执行结果是绝对正确的,我们是采用一个线程使用同步代码块,另一个线程使用同步函数的方式,看是否会发生数据错误,作为对比,下面的代码中同步代码块我们不使用this,而是使用obj这个对象:

20191210001345\_3.png
20191210001345\_4.png

public class ThreadTrain1 implements Runnable {
        private int tickets = 100;
        private static Object obj = new Object();
        private static boolean flag = true;
        @Override
        public void run() {
            if (flag) {
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                synchronized (obj) {// 同步代码块
                    while (tickets > 0) {
                        if (tickets > 0) {
                            System.out.println(Thread.currentThread().getName() + "卖了第" + (100 - tickets + 1) + "张票");
                            tickets--;
                        }
                    }
                }
            } else {
                while (tickets > 0) {
                    sale();
                }
            }
        }
        public synchronized void sale() {
            if (tickets > 0) {
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
                System.out.println(Thread.currentThread().getName() + "卖了第" + (100 - tickets + 1) + "张票");
                tickets--;
            }
        }
        public static void main(String[] args) {
            ThreadTrain1 tt = new ThreadTrain1();
            Thread th1 = new Thread(tt, "1号窗口");
            Thread th2 = new Thread(tt, "2号窗口");
            th1.start();
            try {
                Thread.sleep(40);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            ThreadTrain1.flag = false;
            th2.start();
        }
    }

View Code

显然,这段代码最后会出现数据冲突的情况,因为两个线程拿到的不是同一把锁,也证明了同步函数锁的是this。

明白了同步函数的锁是this,那么加上static以后,锁的对象会不会发生改变,还是依然是this???

先锁this,验证是否是this:

20191210001345\_5.png
20191210001345\_6.png

public class ThreadTrain1 implements Runnable {
        private static int tickets = 100;
        private static boolean flag = true;
        @Override
        public void run() {
            if (flag) {
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                synchronized (this) {// 同步代码块
                    while (tickets > 0) {
                        if (tickets > 0) {
                            System.out.println(Thread.currentThread().getName() + "卖了第" + (100 - tickets + 1) + "张票");
                            tickets--;
                        }
                    }
                }
            } else {
                while (tickets > 0) {
                    sale();
                }
            }
        }
        public static synchronized void sale() {
            if (tickets > 0) {
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
                System.out.println(Thread.currentThread().getName() + "卖了第" + (100 - tickets + 1) + "张票");
                tickets--;
            }
        }
        public static void main(String[] args) {
            ThreadTrain1 tt = new ThreadTrain1();
            Thread th1 = new Thread(tt, "1号窗口");
            Thread th2 = new Thread(tt, "2号窗口");
            th1.start();
            try {
                Thread.sleep(40);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            ThreadTrain1.flag = false;
            th2.start();
        }
    }

View Code

出现了数据错误,这里我们不做猜测,只做验证,静态的同步函数锁的是当前类的字节码文件,代码验证:

20191210001345\_7.png
20191210001345\_8.png

public class ThreadTrain1 implements Runnable {
        private static int tickets = 100;
        private static boolean flag = true;
        @Override
        public void run() {
            if (flag) {
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                synchronized (ThreadTrain1.class) {// 同步代码块
                    while (tickets > 0) {
                        if (tickets > 0) {
                            System.out.println(Thread.currentThread().getName() + "卖了第" + (100 - tickets + 1) + "张票");
                            tickets--;
                        }
                    }
                }
            } else {
                while (tickets > 0) {
                    sale();
                }
            }
        }
        public static synchronized void sale() {
            if (tickets > 0) {
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
                System.out.println(Thread.currentThread().getName() + "卖了第" + (100 - tickets + 1) + "张票");
                tickets--;
            }
        }
        public static void main(String[] args) {
            ThreadTrain1 tt = new ThreadTrain1();
            Thread th1 = new Thread(tt, "1号窗口");
            Thread th2 = new Thread(tt, "2号窗口");
            th1.start();
            try {
                Thread.sleep(40);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            ThreadTrain1.flag = false;
            th2.start();
        }
    }

View Code

多线程死锁

同步中嵌套同步,锁没有来得及释放,一直等待,就导致死锁。

下面这段代码,多运行几次就会出现死锁,思路是开启两个线程,让这两个线程执行的代码获取的锁的顺序不同,第一个线程需要先获得obj对象锁,然后再获得this锁,才可以执行代码,然后释放两把锁。线程2需要先获得this锁,再获取obj对象锁才可执行代码,然后释放两把锁。但是,当线程1获得了obj锁之后,线程2获得了this锁,这时候线程1需要获得this锁才可执行,但是线程2也无法获取到obj对象锁执行代码并释放,所以两个线程都拿着一把锁不释放,这就产生了死锁。

20191210001345\_9.png
20191210001345\_10.png

public class ThreadTrain3 implements Runnable {
        private static int tickets = 100;
        private static Object obj = new Object();
        private static boolean flag = true;
        @Override
        public void run() {
            if (flag) {
                while (true) {
                    System.out.println("111111");
                    synchronized (obj) {// 同步代码块
                        sale();
                    }
                }
            } else {

                while (true) {
                    System.out.println(222222);
                    sale();
                }
            }
        }
        public synchronized void sale() {
            synchronized(obj){
            if (tickets > 0) {
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "卖了第" + (100 - tickets + 1) + "张票");
                tickets--;
            }
            }
        }
        public static void main(String[] args) {
            ThreadTrain3 tt = new ThreadTrain3();
            Thread th1 = new Thread(tt, "1号窗口");
            Thread th2 = new Thread(tt, "2号窗口");
            th1.start();
            try {
                Thread.sleep(40);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            ThreadTrain3.flag = false;
            System.out.println(flag);
            th2.start();
        }
    }

View Code

多线程的三大特性

原子性

原子性就是在执行一个或者多个操作的过程中,要么全部执行完不被任何因素打断,要么不执行。比如银行转账,A账户减去100元,B账户必须增加100元,对这两个账户的操作必须保证原子性,才不会出现问题。还有比如:i=i+1的操作,需要先取出i,然后对i进行+1操作,然后再给i赋值,这个式子就不是原子性的,需要同步来实现数据的安全。

原子性就是为了保证数据一致,线程安全。

可见性

当多个线程访问同一个变量时,一个线程修改了变量的值,其他的线程能立即看到,这就是可见性。

这里讲一下Java内存模型?简称JMM,决定了一个线程与另一个线程是否可见,包括主内存(存放共享的全局变量)和私有本地内存(存放本地线程私有变量)

20191210001345\_11.png

本地私有内存存放的是共享变量的副本,线程操作共享变量,首先操作的是自己本地内存的副本,当同一时刻只有一个线程操作共享变量时,该线程操作完毕本地内存,然后会刷新到主内存,然后主内存会通知另一个线程,进而更新;但是如果同一时刻有多个线程操作共享变量,会来不及更新主内存进而通知其他线程更新变量,就会出现冲突问题。

有序性

就是程序的执行顺序会按照代码先后顺序进行执行,一般情况下,处理器由于要提高执行效率,对代码进行重排序,运行的顺序可能和代码先后顺序不同,但是结果一样。单线程下不会出现问题,多线程就会出现问题了。

volatile

保证可见性,但是不保证原子性。

下面这个案例10个线程共享同一个count,进行+1操作:

public class VolatileTest extends Thread{
        private volatile static int count = 0;
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                count++;
            }
            System.out.println(Thread.currentThread().getName()+":"+count);
        }
        public static void main(String[] args) {
            VolatileTest[] list = new VolatileTest[10];
            for (int i = 0; i < list.length; i++) {
                list[i] = new VolatileTest();
            }
            for (int i = 0; i < list.length; i++) {
                list[i].start();
            }
        }
    }

多运行几次,就会出现最后结果有不到1000的情况,也就证明了volatile不会保证原子性。

保证原子性,jdk1.5之后,并发包提供了很多原子类,例如AtomicInteger :

public class VolatileTest2 extends Thread{
        private static AtomicInteger count = new AtomicInteger();
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                count.incrementAndGet();
            }
            System.out.println(Thread.currentThread().getName()+":"+count.get());
        }
        public static void main(String[] args) {
            VolatileTest2[] list = new VolatileTest2[10];
            for (int i = 0; i < list.length; i++) {
                list[i] = new VolatileTest2();
            }
            for (int i = 0; i < list.length; i++) {
                list[i].start();
            }
        }
    }

AtomicInteger解决了同步, 最后的结果最大的肯定是1000
点赞(0)
版权归原创作者所有,任何形式转载请联系作者; Java 技术驿站 >> 多线程安全(synchronized、三大特性、Java内存模型)

相关推荐