JUC-sysnchronized详解(三)

一、前言

线程锁是什么?有什么用?怎么用?
先来看一段代码:

public class MyService{
    private String username = "AA";
    private String password = "aa";

	//非线程安全的get方法
    public void getValue() {
        System.out.println(Thread.currentThread().getName()+" : "+username+" "+password);
    }

	//线程安全的set方法
	//设置username值停顿1s再设置password值
    synchronized public void setValue(String username,String password){
        this.username = username;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.password = password;
    }

    public static void main(String[] args) throws InterruptedException {
        MyService service = new MyService();
		
		//先启动线程A,200毫秒后再启动线程B
        Thread thread1 = new Thread(() -> service.setValue("BB","bb"),"Thread-A");
        thread1.start();
        Thread.sleep(200);
        Thread thread2 = new Thread(service::getValue,"Thread-B");
        thread2.start();
    }
}

打印结果:

Thread-B : BB aa

出现脏读是因为 getValue 方法不是同步的,所以可以在任意时候进行调用。解决方法就是加上同步 synchronized 关键字,代码如下:

synchronized public void getValue() {
   System.out.println(Thread.currentThread().getName()+" : "+username+" "+password);
}

运行结果:

Thread-B : BB bb

下面开始介绍sysnchronized的特性和使用。

二、特性

2.1 synchronized 锁重入

关键字 synchronized 拥有锁重入的功能,也就是在使用 synchronized 时,当一个线程得到一个锁后,再次请求获取此锁时是可以再次得到该锁的。

这也证明了在一个 synchronized 方法 / 块的内部调用本类的其他 synchronized 方法 / 块,若锁一样,是可以得到锁的。

示例代码:


public class MyService{
    synchronized public void service1(){
        System.out.println("service1");
        service2();
    }

    synchronized public void service2(){
        System.out.println("service2");
    }
}

或

public class MyService{
    synchronized public void service1(){
        System.out.println("service1");
		Thread.sleep(1000);
        Service1();
    }
}

“可重入锁”通俗的理解就是:线程自己可以再次获取自己的当前持有的锁。可重入锁也支持在父子类继承的环境中。

示例代码:

public class MyServiceChild extends MyService{
synchronized public void service(){
System.out.println(“service1”);
this.service2();
}
}

2.2 不具有继承性

同步不可以继承。子类继承父类的同步方法时还需要添加 synchronized 关键字才能保持同步。

三、sysnchronized的锁对象

3.1 同步静态方法

示例:

public class ThreadTest { 
    synchronized public static void service() {     
        System.out.println(Thread.currentThread().getName() + " begin: " + System.currentTimeMillis());      
        try {          
            Thread.sleep(3000);   
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }       
        System.out.println(Thread.currentThread().getName() + " end: " + System.currentTimeMillis());
    }  
    synchronized public static void service2(){     
        System.out.println(Thread.currentThread().getName() + " begin: " + System.currentTimeMillis());  
        try {      
            Thread.sleep(3000);      
        } catch (InterruptedException e) {    
            e.printStackTrace();     
        }       
        System.out.println(Thread.currentThread().getName() + " end: " + System.currentTimeMillis());  
    }    
    public static void main(String[] args) {   
        new Thread(new Runnable() {        
            @Override           
            public void run() {    
                ThreadTest.service();  
            }
        }, "Thread-A").start(); 
        
        new Thread(new Runnable() {    
            @Override          
            public void run() {   
                new ThreadTest().service2();   
            }  
        }, "Thread-B").start(); 
    }
}

结果

Thread-A begin: 1612403849173
Thread-A end: 1612403852173
Thread-B begin: 1612403852173
Thread-B end: 1612403855174

小结
由上面示例可知,线程A执行完了,才执行线程B。原因是两个方法都定义为静态同步方法,因此两个方法的锁对象都为当前类的class对象,所以两个方法是同步的。

3.2 同步普通方法

代码

public class ThreadTest { 
    synchronized public void service() {  
        System.out.println(Thread.currentThread().getName() + " begin: " + System.currentTimeMillis());      
        try {          
            Thread.sleep(3000);      
        } catch (InterruptedException e) {    
            e.printStackTrace();       
        }       
        System.out.println(Thread.currentThread().getName() + " end: " + System.currentTimeMillis());
    }    
    synchronized public void service2(){  
        System.out.println(Thread.currentThread().getName() + " begin: " + System.currentTimeMillis());    
        try {           
            Thread.sleep(3000);    
        } catch (InterruptedException e) { 
            e.printStackTrace();   
        }       
        System.out.println(Thread.currentThread().getName() + " end: " + System.currentTimeMillis());   
    }    
    public static void main(String[] args) {      
        new Thread(new Runnable() {       
            @Override          
            public void run() {          
                new ThreadTest().service();   
            }        
        }, "Thread-A").start();   
        
        new Thread(new Runnable() {      
            @Override           
            public void run() {        
                new ThreadTest().service2();   
            }       
        }, "Thread-B").start();  
    }
}

结果

Thread-A begin: 1612404203824
Thread-B begin: 1612404203829
Thread-A end: 1612404206825
Thread-B end: 1612404206830

小结
由上面示例可知,两个方法是异步执行的,互不干预执行,原因是普通方法的锁对象是当前类的实例对象,因此两个线程调用的分别是两个不同的实例方法,锁不同,所以是异步的。

当我们将main方法改写如下:

public static void main(String[] args) { 
   ThreadTest2 threadTest2 = new ThreadTest2();    
   new Thread(new Runnable() {        
   		@Override        
   		public void run() {            
   			threadTest2.service();        
		}    
	}, "Thread-A").start();    

	new Thread(new Runnable() {
       @Override       
       public void run() {
              threadTest2.service2();        
              }    
    }, "Thread-B").start();
}

两个方法的执行就因此变成了同步,因为,他们的锁对象都是同一个实例。

3.3 同步代码块

介绍
同步代码块的锁对象可以是任意对象,因此比较特殊。

public void service() {    
	synchronized(XX){        
		System.out.println(Thread.currentThread().getName() + " begin: " + System.currentTimeMillis());        
		try {            
			Thread.sleep(3000); 
    	} catch (InterruptedException e) {            
       		e.printStackTrace();       
        }       
        
        System.out.println(Thread.currentThread().getName() + " end: " + System.currentTimeMillis());  
    }
}
public void service2(){    
	synchronized(XX){        
		System.out.println(Thread.currentThread().getName() + " begin: " + System.currentTimeMillis());        
		try {            
			Thread.sleep(3000);        
		} catch (InterruptedException e) {            
			e.printStackTrace();        
		}        
		System.out.println(Thread.currentThread().getName() + " end: " + System.currentTimeMillis());    
	}
}
3.3.1 锁对象为this

这时需要注意线程调用同步方法使用的实例,如下代码:

public static void main(String[] args) { 
  	new Thread(new Runnable() {
	   @Override        
	   public void run() {      
	         new ThreadTest3().service();        
	    }   
	 }, "Thread-A").start();   
	 
 	 new Thread(new Runnable() {     
	     @Override        
	     public void run() {        
	         new ThreadTest3().service2();        
	     }    
	  }, "Thread-B").start();
}

可知,两个方法的锁对象,使用的不是同一个实例,因此是异步执行。
改写:

public static void main(String[] args) {
    ThreadTest2 threadTest2 = new ThreadTest3();    
    new Thread(new Runnable() {
            @Override        
            public void run() {      
                  threadTest2.service();        
            }    
     }, "Thread-A").start();    

	new Thread(new Runnable() {   
	     @Override        
	     public void run() {      
	           threadTest2.service2();        
         }    
   	}, "Thread-B").start();}

可知,两个方法同步代码块使用的锁对象是同一个实例,因此是同步执行。

3.3.2 锁对象为this.getClass

首先我们需要知道,任意类都对应有且仅有一个Class对象(单例的),不论由哪个实例对象get出来的class,其实都是同一个。
因此,只要该类下,同步代码块锁对象为这个类的class对象,都会时同步执行。

3.3.3 锁对象为变量

前提知识:
Java有三种类型变量:局部、成员、静态
字面量的变量如下:
public String lock = “lock”;
Integer lock = 1;
Character lock = ‘l’;
非字面量如下:
Integer i = new Integer(1);
String lock = new String(“lock”);
简言之,八种包装类型都可以定义字面值(不是new出来的)。
字面值会保存在方法区的常量池中(会被共用),而通过new产生的对象存在堆中。

进入正题:

public class ThreadTest4 {  
    public String lock = "lock";   
    public String lock2 = "lock";  
    public void service() {     
        synchronized(lock){    
            System.out.println(Thread.currentThread().getName() + " "+lock.hashCode()); 
            System.out.println(Thread.currentThread().getName() + " begin: " + System.currentTimeMillis()); 
            try {             
                Thread.sleep(3000);       
            } catch (InterruptedException e) {  
                e.printStackTrace();        
            }         
            System.out.println(Thread.currentThread().getName() + " end: " + System.currentTimeMillis());     
        }   
    }    
    public void service2(){   
        synchronized(lock2){   
            System.out.println(Thread.currentThread().getName() + " "+lock2.hashCode());   
            System.out.println(Thread.currentThread().getName() + " begin: " + System.currentTimeMillis());     
            try {       
                Thread.sleep(3000);         
            } catch (InterruptedException e) {    
                e.printStackTrace();     
            }       
            System.out.println(Thread.currentThread().getName() + " end: " + System.currentTimeMillis());     
        }   
    }   
    public static void main(String[] args) {     
        new Thread(new Runnable() {     
            @Override         
            public void run() {      
                new ThreadTest4().service();
            }    
        }, "Thread-A").start();       
        new Thread(new Runnable() {       
            @Override       
            public void run() {     
                new ThreadTest4().service2();   
            }     
        }, "Thread-B").start();  
    }
}

运行结果,是同步的。

可见,两个方法的对象锁都是等值的String类型数据,根据上述字面值知识,可知两个方法使用的锁对象是同样的。

更改如下:
public String lock = “lock”;
public String lock2 = new String(“lock”);
运行结果,是异步的。
可知两个方法的锁对象不一样。

再将两个方法锁改如下:
public void service(String lock) {
//…
}
public void service2(String lock) {
//…
}
线程调用时,两个线程都直接传入值“1”
结果可知,等值时,同步,不等值时,异步。可见两个局部变量,也是共享常量池中的同一个字面值,因此他们的锁对象是一样的。

再改,两个线程都传入:new String(“1”)
结果可知,异步。因为对象存在堆中,不是线程共享的,因此锁对象不一样。

同理的,其他几种基本数据包装类型,也有次特性。

小结:
成员、局部变量的字面值缓存在常量池中,会被线程共享,因此用于作锁对象时,等值时会被认为同一个锁。
静态变量(不论字面值还是引用类型),会存在方法区中,且仅能有一个,被线程共享。
由于常量池的特性,容易出现问题,因此,一般推荐使用字面值作为锁对象。推荐 new Object() 实例化一个 Object 对象,但它并不放入缓存中。

四、锁的改变

代码:

class ThreadTest5 {
    private String lock = "123";
    public void service(){
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName()+" begin: " + System.currentTimeMillis());
            lock = "456";
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+" end: " + System.currentTimeMillis());
        }
    } 
    public static void main(String[] args) throws InterruptedException {
        ThreadTest5 test = new ThreadTest5();
        new Thread(new Runnable() {
            @Override
            public void run() {
                test.service();
            }
        },"Thread-A").start();
        Thread.sleep(50);
        new Thread(new Runnable() {
            @Override
            public void run() {
                test.service();
            }
        },"Thread-B").start();
    }
}

分析:
运行结果:

Thread-A begin: 1537019992452
Thread-B begin: 1537019992652
Thread-A end: 1537019994453
Thread-B end: 1537019994653

为什么是异步?因为 50ms 过后,第二個线程取得的锁是“456”,所以两个线程持有的是不同的,故是异步执行。

把 lock = “456” 放在 Thread.sleep(2000) 之后,再次运行。

Thread-A begin: 1537020101553
Thread-A end: 1537020103554
Thread-B begin: 1537020103554
Thread-B end: 1537020105558
线程 A 和线程 B 持有的锁都是“123”,虽然将锁改成了“456”,但结果还是同步的,因为 B 再锁被更改前启动了,争抢的锁是“123”。

还需要提示一下,对于锁为复杂对象(含属性值),只要对象不变,即使对象的属性被改变,运行的结果还是同步的。

五、sysnchronized造成的多线程死锁

案例:


class ThreadTest6 implements Runnable {   
    public String username;   
    public Object locak1 = new Object();  
    public Object locak2 = new Object();
    public void setFlag(String username){  
        this.username = username;   
    }  
    @Override   
    public void run() {  
        if (username.equals("a")){    
            synchronized (locak1){    
                System.out.println("username:"+username);       
                try {                    
                    Thread.sleep(3000);        
                } catch (InterruptedException e) {    
                    e.printStackTrace();            
                }            
                synchronized (locak2){         
                    System.out.println(" 按 lock1-》lock2 执行 ");  
                }          
            }     
        }        
        if (username.equals("b")){    
            synchronized (locak2){   
                System.out.println("username:"+username);        
                try {              
                    Thread.sleep(3000);         
                } catch (InterruptedException e) {       
                    e.printStackTrace();         
                }        
                synchronized (locak1){         
                    System.out.println(" 按 lock2-》lock1 执行 "); 
                }        
            }    
        }  
    }
    public static void main(String[] args) throws InterruptedException {   
        ThreadTest6 dealThread = new ThreadTest6();   
        dealThread.setFlag("a");  
        Thread threadA = new Thread(dealThread);       
        threadA.start();       
        Thread.sleep(100);    
        dealThread.setFlag("b");       
        Thread threadB = new Thread(dealThread);    
        threadB.start();   
    }
}
    
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值