• Java并发基础--线程安全


    一、线程安全

    1.线程安全的概念

    线程安全:某个类被单个线程,或者多个线程同时访问,所表现出来的行为是一致,则可以说这个类是线程安全的。

    2.什么情况下会出现线程安全问题

    在单线程中不会出现线程安全问题,在多线程编程的情况下,并且多个线程访问同一资源的情况下可能出现线程安全问题。如下面的例子,出现典型的线程安全问题:

     1 public class BookSaleRunable implements Runnable{
     2     private int bookNum=10;//书的总数为10
     3     @Override
     4     public void run() {
     5         for(int i=0;i<5;i++){
     6             if(bookNum>0){
     7                 try {
     8                     Thread.sleep(3000);
     9                 } catch (InterruptedException e) {
    10                     e.printStackTrace();
    11                 }
    12                 System.out.println(Thread.currentThread().getName()+" 卖出一本书,剩余书籍:"+(--bookNum));
    13             }
    14         }
    15         
    16     }
    17     
    18     public static void main(String[] args) {
    19         BookSaleRunable booksr = new BookSaleRunable();
    20         //开启3个线程
    21         Thread t1 = new Thread(booksr);
    22         Thread t2 = new Thread(booksr);
    23         Thread t3 = new Thread(booksr);
    24         t1.start();
    25         t2.start();
    26         t3.start();
    27     }
    28 
    29 }

    结果输出:

    Thread-1 卖出一本书,剩余书籍:9
    Thread-2 卖出一本书,剩余书籍:7
    Thread-0 卖出一本书,剩余书籍:8
    Thread-1 卖出一本书,剩余书籍:6
    Thread-0 卖出一本书,剩余书籍:4
    Thread-2 卖出一本书,剩余书籍:5
    Thread-1 卖出一本书,剩余书籍:3
    Thread-2 卖出一本书,剩余书籍:1
    Thread-0 卖出一本书,剩余书籍:2
    Thread-2 卖出一本书,剩余书籍:0
    Thread-1 卖出一本书,剩余书籍:-1
    Thread-0 卖出一本书,剩余书籍:-2

    上例出现超卖现象,在生活中不允许,同时如果是一个线程肯定不会出现。那么如何解决这个问题呢?基本上所有的并发模式在解决线程冲突问题的时候,都是采用序列化访问共享资源的方案,在给定时刻只允许一个任务访问共享资源。

    二、解决线程安全问题的方法

    1.synchronized关键字

    synchronized,顾名思义就是同步的意思,主要用来给方法、代码块加锁。当某个方法或者代码块使用synchronized时,那么在同一时刻至多仅有有一个线程在执行该段代码。当有多个线程访问同一对象的加锁方法/代码块时,同一时间只有一个线程在执行,其余线程必须要等待当前线程执行完之后才能执行该代码段。但是,其余线程是可以访问该对象中的非加锁代码块的。synchronized可以用来修饰方法和代码块。

    synchronized修饰方法

    当synchronized关键字修饰方法的时候,这个方法我们称为同步方法,同步方法可以控制对类成员变量的方法,synchronized关键字表明方法已经加锁,当任意一个线程访问同步方法的时候都必须 判断这个方法是否被其他线程独占。所有对象自动含有单一的锁,当在对象上调用其任意的synchronized方法的时候,次对象被加锁,这个时候这个对象的其他synchronized方法必须等到前一个同步方法调用完毕并释放锁之后才能被调用。如下:

     1 public class BookSaleRunable2 implements Runnable{
     2     private int bookNum=20;//书的总数为10
     3     @Override
     4     public void run() {
     5         for(int i=0;i<7;i++){
     6             saleBook();
     7         }
     8         
     9     }
    10     
    11     public static void main(String[] args) {
    12         BookSaleRunable2 booksr = new BookSaleRunable2();
    13         //开启3个线程
    14         Thread t1 = new Thread(booksr);
    15         Thread t2 = new Thread(booksr);
    16         Thread t3 = new Thread(booksr);
    17         t1.start();
    18         t2.start();
    19         t3.start();
    20     }
    21     
    22     private synchronized void saleBook(){
    23         if(bookNum>0){
    24             try {
    25                 Thread.sleep(300);
    26             } catch (InterruptedException e) {
    27                 e.printStackTrace();
    28             }
    29             System.out.println(Thread.currentThread().getName()+" 卖出一本书,剩余书籍:"+(--bookNum));
    30         }
    31     }
    32 
    33 }

    结果输出:

    Thread-0 卖出一本书,剩余书籍:19
    Thread-0 卖出一本书,剩余书籍:18
    Thread-0 卖出一本书,剩余书籍:17
    Thread-0 卖出一本书,剩余书籍:16
    Thread-0 卖出一本书,剩余书籍:15
    Thread-0 卖出一本书,剩余书籍:14
    Thread-0 卖出一本书,剩余书籍:13
    Thread-2 卖出一本书,剩余书籍:12
    Thread-2 卖出一本书,剩余书籍:11
    Thread-2 卖出一本书,剩余书籍:10
    Thread-2 卖出一本书,剩余书籍:9
    Thread-2 卖出一本书,剩余书籍:8
    Thread-2 卖出一本书,剩余书籍:7
    Thread-2 卖出一本书,剩余书籍:6
    Thread-1 卖出一本书,剩余书籍:5
    Thread-1 卖出一本书,剩余书籍:4
    Thread-1 卖出一本书,剩余书籍:3
    Thread-1 卖出一本书,剩余书籍:2
    Thread-1 卖出一本书,剩余书籍:1
    Thread-1 卖出一本书,剩余书籍:0

    注意:在使用并发的时候,将域设置为private是非常重要的,否则,synchronized关键字不能防止其他任务直接访问域,这样讲产生冲突问题。

    synchronized与static

    关键字synchronized可以修饰static静态方法,这个时候是对当前静态方法所属的Class类进行加锁。与修饰普通方法是有差别的。如下:

     1 public class ServiceDemo {
     2     
     3     public  static void print1(){
     4         System.out.println(Thread.currentThread().getName()+" print1 start ");
     5         System.out.println(Thread.currentThread().getName()+" print1 end ");
     6     }
     7     public  static void print2(){
     8         System.out.println(Thread.currentThread().getName()+" print2 start ");
     9         System.out.println(Thread.currentThread().getName()+" print2 end ");
    10     }
    11     
    12     public static void main(String[] args) {
    13         ServiceDemo serviceDemo1 = new ServiceDemo();
    14         ServiceDemo serviceDemo2 = new ServiceDemo();
    15         Thread t1 =new Thread(new Thread1(serviceDemo1));
    16         Thread t2 =new Thread(new Thread2(serviceDemo2));
    17         t1.start();
    18         t2.start();
    19     }
    20 }
    21 
    22 class Thread1 implements Runnable{
    23     private ServiceDemo serviceDemo;
    24     
    25     Thread1(ServiceDemo serviceDemo){
    26         this.serviceDemo = serviceDemo;
    27     }
    28     
    29     @Override
    30     public void run() {
    31         serviceDemo.print1();
    32     }
    33     
    34 }
    35 class Thread2 implements Runnable{
    36     private ServiceDemo serviceDemo;
    37     
    38     Thread2(ServiceDemo serviceDemo){
    39         this.serviceDemo = serviceDemo;
    40     }
    41     
    42     @Override
    43     public void run() {
    44         serviceDemo.print2();
    45     }
    46     
    47 }

    输出结果:

    Thread-0 print1 start 
    Thread-1 print2 start 
    Thread-0 print1 end 
    Thread-1 print2 end 

    使用static和synchronized结合的时候,效果完全不同了,如下:

     1 public class ServiceDemo {
     2     
     3     public synchronized static void print1(){
     4         System.out.println(Thread.currentThread().getName()+" print1 start ");
     5         System.out.println(Thread.currentThread().getName()+" print1 end ");
     6     }
     7     public synchronized static void print2(){
     8         System.out.println(Thread.currentThread().getName()+" print2 start ");
     9         System.out.println(Thread.currentThread().getName()+" print2 end ");
    10     }
    11     
    12     public static void main(String[] args) {
    13         ServiceDemo serviceDemo1 = new ServiceDemo();
    14         ServiceDemo serviceDemo2 = new ServiceDemo();
    15         Thread t1 =new Thread(new Thread1(serviceDemo1));
    16         Thread t2 =new Thread(new Thread2(serviceDemo2));
    17         t1.start();
    18         t2.start();
    19     }
    20 }
    21 
    22 class Thread1 implements Runnable{
    23     private ServiceDemo serviceDemo;
    24     
    25     Thread1(ServiceDemo serviceDemo){
    26         this.serviceDemo = serviceDemo;
    27     }
    28     
    29     @Override
    30     public void run() {
    31         serviceDemo.print1();
    32     }
    33     
    34 }
    35 class Thread2 implements Runnable{
    36     private ServiceDemo serviceDemo;
    37     
    38     Thread2(ServiceDemo serviceDemo){
    39         this.serviceDemo = serviceDemo;
    40     }
    41     
    42     @Override
    43     public void run() {
    44         serviceDemo.print2();
    45     }
    46     
    47 }

    输出结果:

    Thread-0 print1 start 
    Thread-0 print1 end 
    Thread-1 print2 start 
    Thread-1 print2 end 

    从结果看出,两个线程并未共享同一个对象,但是两个不同对象的静态同步方法发生了同步互斥。如果将ServiceDemo类中的一个方法修改为非静态同步方法,上述代码的执行结果则可能不是上面显示的那样了,那是因为如果一个线程执行一个对象的非static synchronized方法,另外一个线程需要执行这个对象所属类的static synchronized方法,此时不会发生互斥现象,因为访问static synchronized方法占用的是类锁,而访问非static synchronized方法占用的是对象锁,所以不存在互斥现象。

    synchronized修饰代码块

    使用关键字synchronized声明方法在某些情况下是有弊端的,非常影响效率。如果多个线程在访问一个synchronized方法,那么同一时刻只有一个线程在执行该方法,而其他线程都必须等待,但是如果该方法没有使用synchronized,则所有线程可以在同一时刻执行它,减少了执行的总时间。在这样的情况下,推荐使用同步语句块来解决,同步方法是对当前对象进行加锁,而synchronized代码块是对某一个对象加锁。synchronized代码块使用起来比synchronized方法要灵活得多。因为也许一个方法中只有一部分代码只需要同步,如果此时对整个方法用synchronized进行同步,会影响程序执行效率。

     1 public class BookSaleRunable3 implements Runnable{
     2     private int bookNum=20;//书的总数为10
     3     @Override
     4     public void run() {
     5         for(int i=0;i<7;i++){
     6             synchronized (this) {
     7                 if(bookNum>0){
     8                     try {
     9                         Thread.sleep(300);
    10                     } catch (InterruptedException e) {
    11                         e.printStackTrace();
    12                     }
    13                     System.out.println(Thread.currentThread().getName()+" 卖出一本书,剩余书籍:"+(--bookNum));
    14                 }
    15             }
    16         }
    17         
    18     }
    19     
    20     public static void main(String[] args) {
    21         BookSaleRunable3 booksr = new BookSaleRunable3();
    22         //开启3个线程
    23         Thread t1 = new Thread(booksr);
    24         Thread t2 = new Thread(booksr);
    25         Thread t3 = new Thread(booksr);
    26         t1.start();
    27         t2.start();
    28         t3.start();
    29     }
    30 
    31 }

    结果输出:

    Thread-0 卖出一本书,剩余书籍:19
    Thread-0 卖出一本书,剩余书籍:18
    Thread-0 卖出一本书,剩余书籍:17
    Thread-0 卖出一本书,剩余书籍:16
    Thread-0 卖出一本书,剩余书籍:15
    Thread-0 卖出一本书,剩余书籍:14
    Thread-0 卖出一本书,剩余书籍:13
    Thread-2 卖出一本书,剩余书籍:12
    Thread-2 卖出一本书,剩余书籍:11
    Thread-2 卖出一本书,剩余书籍:10
    Thread-2 卖出一本书,剩余书籍:9
    Thread-1 卖出一本书,剩余书籍:8
    Thread-1 卖出一本书,剩余书籍:7
    Thread-1 卖出一本书,剩余书籍:6
    Thread-1 卖出一本书,剩余书籍:5
    Thread-1 卖出一本书,剩余书籍:4
    Thread-1 卖出一本书,剩余书籍:3
    Thread-1 卖出一本书,剩余书籍:2
    Thread-2 卖出一本书,剩余书籍:1
    Thread-2 卖出一本书,剩余书籍:0

    总结:

    • 当一个线程访问对象的一个synchronized方法的时候,另外的线程仍然可以访问对象的非synchronized方法。
    • 当一个线程正在访问一个对象的synchronized方法,那么其他线程不能访问该对象的其他synchronized方法。
    • 同一时间只有一个线程可以执行synchronized同步代码块中的代码。
    • 如果一个线程执行一个对象的非static synchronized方法,另外一个线程需要执行这个对象所属类的static synchronized方法,此时不会发生互斥现象,因为访问static synchronized方法占用的是类锁,而访问非static synchronized方法占用的是对象锁,所以不存在互斥现象。
    • synchronized方法或者synchronized代码块,当出现异常时,JVM会自动释放当前线程占用的锁,因此不会由于异常导致出现死锁现象

    2.Lock锁机制

    Lock对象必须被显式的创建、锁定和释放,与jvm内建的锁相比,代码缺乏优雅性,但是对于解决某些特殊类型的问题来说,它更加灵活。使用synchronized关键字,需要些的代码量更少。具体的例子如下:

     1 public class BookSaleRunable4 implements Runnable {
     2     private int bookNum = 10;// 书的总数为10
     3     private Lock lock = new ReentrantLock();// 重入锁
     4 
     5     @Override
     6     public void run() {
     7         for (int i = 0; i < 3; i++) {
     8             lock.lock();// 显示加锁
     9             try {
    10                 if (bookNum > 0) {
    11                     Thread.sleep(300);
    12                     System.out.println(Thread.currentThread().getName()+" 卖出一本书,剩余书籍:" + (--bookNum));
    13                 }
    14             } catch (InterruptedException e) {
    15                 e.printStackTrace();
    16             }finally{
    17                 lock.unlock();//主动释放锁,很容易忘记释放锁
    18             }
    19         }
    20 
    21     }
    22     public static void main(String[] args) {
    23         BookSaleRunable4 booksr = new BookSaleRunable4();
    24         // 开启3个线程
    25         Thread t1 = new Thread(booksr);
    26         Thread t2 = new Thread(booksr);
    27         Thread t3 = new Thread(booksr);
    28         t1.start();
    29         t2.start();
    30         t3.start();
    31     }
    32 
    33 }

    结果输出:

    Thread-0 卖出一本书,剩余书籍:9
    Thread-0 卖出一本书,剩余书籍:8
    Thread-0 卖出一本书,剩余书籍:7
    Thread-2 卖出一本书,剩余书籍:6
    Thread-2 卖出一本书,剩余书籍:5
    Thread-2 卖出一本书,剩余书籍:4
    Thread-1 卖出一本书,剩余书籍:3
    Thread-1 卖出一本书,剩余书籍:2
    Thread-1 卖出一本书,剩余书籍:1
  • 相关阅读:
    learn go memoization
    learn go return fuction
    Java5 并发学习
    Java中的protected访问修饰符
    LinkedBlockingQueue
    BlockingQueue的使用
    Java并发编程:Callable、Future和FutureTask
    Java线程池的工作原理与实现
    SQL Server中Delete语句表名不能用别名
    请问JDBC中IN语句怎么构建
  • 原文地址:https://www.cnblogs.com/liupiao/p/9326396.html
Copyright © 2020-2023  润新知