2.3多线程(java学习笔记)synchronized关键字

一、为什么要用synchronized关键字

首先多线程中多个线程运行面临共享数据同步的问题。

多线程正常使用共享数据时需要经过以下步骤:

1.线程A从共享数据区中复制出数据副本,然后处理。

2.线程A将处理好的数据副本写入共享数据区。

3.线程B从共享数据区中复制出数据副本。

如此循环,直到线程结束。

 假如线程A从共享数据区中复制出数据副本然后处理,在还没有将更新的数据放入主内存时,线程B来到主内存读取了未更新的数据,这样就出问题了。

这就是所谓的脏读,这类问题称为多线程的并发问题。

举个具体的例子:

 1 public class TestThread {
 2      public static void main(String[] args){
 3          TestSynchronized s = new TestSynchronized();
 4          new Thread(s,"t1").start();    //两个线程访问一个对象
 5          new Thread(s,"t2").start();
 6      }
 7 }
 8 
 9 class TestSynchronized implements Runnable{
10     private int ticket = 5;
11     
12     public void run(){    
13             for(int p = 0; p < 10; p++){
14                 try {
15                     Thread.sleep(500);
16                 } catch (InterruptedException e) {
17                     // TODO Auto-generated catch block
18                     e.printStackTrace();
19                 }
20                 if(ticket >= 0){
21                     System.out.println(Thread.currentThread().getName() + " ticket:" + ticket--);
22                 }
23             }
24     }
25 }
运行结果:
t2 ticket:4
t1 ticket:5
t1 ticket:2
t2 ticket:3
t1 ticket:1
t2 ticket:1
t2 ticket:0

可以看到1号票同时给了t1和t2,当t1读入1执行了ticket--后,数据还没有来得及写入主内存就被t2从主内存中读走了1,就造成了这种现象。

要想避免这种现象就需要使用synchronized关键字,synchronized英译为同步,我们认为暂且把他看做锁定更好理解。

接下来我们看看synchronized如何使用。

二、synchronized的用法

1. synchronized修饰方法(也称同步方法)

(1) java中每个对象都有一个锁(lock),或者叫做监视器,当前线程访问某个对象中synchronized修饰的方法(同步块)时,线程需要获取到该对象的锁,获取对象锁后才能访问该对象中synchronized方法(同步块),且一个对象中只有一个锁。

(2) 没有获得该对象的锁的其他线程,无法访问该对象中synchronized修饰的方法(同步块)。

(3) 其他线程要想访问该对象中synchronized修饰的方法需要获取该对象的锁。

(4) 对象锁只有将synchronized方法(同步块)中的内容运行完毕或遇到异常才会释放锁。

 例一:

 1 public class TestThread {
 2      public static void main(String[] args){
 3          TestSynchronized s = new TestSynchronized();
 4          new Thread(s,"t1").start();     //两个线程访问一个对象
 5          new Thread(s,"t2").start();
 6      }
 7 }
 8 
 9 class TestSynchronized implements Runnable{
10     private int ticket = 5;
11     
12     synchronized public void run(){    
13             for(int p = 0; p < 10; p++){
14                 try {
15                     Thread.sleep(500);
16                 } catch (InterruptedException e) {
17                     // TODO Auto-generated catch block
18                     e.printStackTrace();
19                 }
20                 if(ticket >= 0){
21                     System.out.println(Thread.currentThread().getName() + " ticket:" + ticket--);
22                 }                    
23             }
24         }
25 }
运行结果:
t1 ticket:5
t1 ticket:4
t1 ticket:3
t1 ticket:2
t1 ticket:1
t1 ticket:0

我们来分析上面程序,首先线程t1进去run方法获得对象s的锁,然后执行完run方法释放锁,run运行忘了也就没有t2的事了。

因为只有将synchronized修饰的方法执行完才会释放锁,故打印五个t1.。

还有一点,如果一个对象里面有多个synchronized方法,某一时刻只能有一个线程进入其中一个synchronized修饰的方法,则这时其他任何线程无法进入该对象中任何一个synchronized修饰的方法。

补充片段:

 1 public class TestThread {
 2     public static void main(String[] args){
 3         Test m1 = new Test();  //两个线程共访问一个对象。
 4 
 5         TestSynchronized_1 s1 = new TestSynchronized_1(m1);
 6         TestSynchronized_2 s2 = new TestSynchronized_2(m1);
 7         new Thread(s1,"t1").start();
 8         new Thread(s2,"t2").start();
 9     }
10 }
11 
12 class Test{
13     synchronized public void test1(){
14                for(int p = 0; p < 5; p++){
15                    System.out.println("s1.run.TestSynchronized_test 1");
16                }   
17     }
18     
19     synchronized public void test2(){
20         for(int p = 0; p < 5; p++){
21             System.out.println("s2.run.TestSynchronized_test 2");
22         }   
23 }
24 }
25 
26 class TestSynchronized_1 implements Runnable{
27    
28    private Test m;
29    public TestSynchronized_1(Test m){
30       this.m = m;
31    }
32    
33    public void run(){
34        m.test1();
35    }
36 }
37 
38 class TestSynchronized_2 implements Runnable{
39        
40        private Test m;
41        public TestSynchronized_2(Test m){
42           this.m = m;
43        }
44        
45        public void run(){
46            m.test2();
47        }
48 }
运行结果:
s1.run.TestSynchronized_test 1
s1.run.TestSynchronized_test 1
s1.run.TestSynchronized_test 1
s1.run.TestSynchronized_test 1
s1.run.TestSynchronized_test 1
s2.run.TestSynchronized_test 2
s2.run.TestSynchronized_test 2
s2.run.TestSynchronized_test 2
s2.run.TestSynchronized_test 2
s2.run.TestSynchronized_test 2

当线程t1运行synchronized修饰的test1方法时,线程t2是无法运行test2方法。结合之前说的,一个对象锁只有一把,而这里是两个线程共享对象(m1),当线程t1获得锁时,线程t2就只能等待。归根结底把握几个要点:

1.锁的唯一性(一个对象只有一把锁,但不同对象就有不同的锁)

2.没锁不能进去入synchronized修饰内容中运行。

3.只有运行完synchronized修饰的内容或遇到异常才释放锁。

我们来看下面这个代码:

例二:

 1 public class TestThread {
 2      public static void main(String[] args){
 3          Mouth m1 = new Mouth();  
 4          Mouth m2 = new Mouth();  
 5          TestSynchronized s1 = new TestSynchronized(m1);//两个线程访问两个对象。
 6          TestSynchronized s2 = new TestSynchronized(m2);
 7          new Thread(s1,"t1").start();  //线程t1
 8          new Thread(s2,"t2").start();   //线程t2
 9      }
10 }
11 
12 class Mouth{  //资源及方法 
13     synchronized public void test(){
14         int ticket = 5;
15         for(int p = 0; p < 10; p++){
16             try {
17                 Thread.sleep(500);
18             } catch (InterruptedException e) {
19                 // TODO Auto-generated catch block
20                 e.printStackTrace();
21             }
22             if(ticket >= 0){
23                 System.out.println(Thread.currentThread().getName() + " ticket:" + ticket--);
24             }                    
25         }
26     }
27 
28 }
29 
30 class TestSynchronized implements Runnable{
31     private Mouth m = new Mouth();
32     
33     public TestSynchronized(Mouth m){
34         this.m = m;
35     }
36     
37     synchronized public void run(){
38         m.test();
39     }
40 }
运行结果:
t1 ticket:5
t2 ticket:5
t2 ticket:4
t1 ticket:4
t1 ticket:3
t2 ticket:3
t2 ticket:2
t1 ticket:2
t1 ticket:1
t2 ticket:1
t2 ticket:0
t1 ticket:0

可以发现好像用synchronized修饰的test方法没有起作用,怎么是t1,t2怎么是交替运行的?

我们回顾下之前说的对象锁,线程获得对象锁后可以访问该对象里面synchronized修饰的方法,其他线程无法访问。

我们上面的代码里面对象有两个,一个是m1、一个是m2。

t1获得了对象m1的锁,然后访问m1中的test方法;t2获得了对象m2的锁,然后访问s2中的test方法。

线程t1和线程t2访问的是不同的资源(m1,m2),并不相互干扰所以没有影响。例一中是因为两个线程访问同一个资源(s1)所以synchronized的起了限制作用。

synchronized修饰方法时只能对多个线程访问同一资源(对象)时起限制作用。

可能大家会说了,那我们有没有办法也限制下这种情况呢,答案当然是可以的。

这就是下面要说的:

2.synchronized修饰静态方法

 当修饰静态方法时锁定的是,而不是对象,我们先把例二修改下看下结果。

例三:

 1 public class TestThread {
 2      public static void main(String[] args){
 3          Mouth m1 = new Mouth();
 4          Mouth m2 = new Mouth();
 5          TestSynchronized s1 = new TestSynchronized(m1);
 6          TestSynchronized s2 = new TestSynchronized(m2); //两个线程访问两个对象
 7          new Thread(s1,"t1").start();
 8          new Thread(s2,"t2").start();
 9      }
10 }
11 
12 class Mouth{
13     synchronized public static void test(){ //改为静态方法,锁定的是类。
14         int ticket = 5;
15         for(int p = 0; p < 10; p++){
16             try {
17                 Thread.sleep(500);
18             } catch (InterruptedException e) {
19                 // TODO Auto-generated catch block
20                 e.printStackTrace();
21             }
22             if(ticket >= 0){
23                 System.out.println(Thread.currentThread().getName() + " ticket:" + ticket--);
24             }                    
25         }
26     }
27 
28 }
29 
30 class TestSynchronized implements Runnable{
31     private Mouth m = new Mouth();
32     
33     public TestSynchronized(Mouth m){
34         this.m = m;
35     }
36     
37     synchronized public void run(){
38         m.test();
39     }
40 }
运行结果:
t1 ticket:5
t1 ticket:4
t1 ticket:3
t1 ticket:2
t1 ticket:1
t1 ticket:0
t2 ticket:5
t2 ticket:4
t2 ticket:3
t2 ticket:2
t2 ticket:1
t2 ticket:0

当synchronized修饰静态方法时,线程需要获得类(Mouth)锁才能运行,没有获得类锁的线程无法运行,且获得类锁的线程会将synchronized修饰的静态方法会运行完毕才释放类锁。

例如例三中的代码,t1先获得类(Mouth)锁运行Mouth类中的test方法,而t2没有类(Mouth)锁就无法运行。一个类只有一个类锁,却可以有多个对象(t1,t2等...)都是一个类(Mouth)中的对象,只要一个线程获取了类(Mouth)锁,其他线程就要等到类锁被释放,然后获得类(Mouth)锁之后才能运行类(Mouth)中synchronized修饰的静态方法。所以即使是两个线程(t1,t2)访问两个不同的资源(m1,m2)也会受到限制,因为m1,m2都属于一个类(Mouth),而锁住类(Mouth)后每次只能有一个线程访问该类(Mouth)中的sychronized修饰的静态方法。

当t1访问m1中的test时,首先获得类(Mouth)锁,这时如果t2访问m2中的test方法时也需要获得类锁,可是这时类锁已经被线程t1获得,故t2无法访问m2中的方法。只有等t1运行完方法中的内容或异常释放锁后t2才有机会获得锁,获得锁后才能运行。

而之前例一中t1,t2锁的是对象,需要结合这几段代码理解下。

3.synchronized块(也称同步块)

如果每次都锁定的范围都是一个方法,每次只能有一个线程进去势必会导致效率的低下,这主要是锁定范围过多引起的。

这时可以根据实际情况锁定合适的区域,这就要用到同步块了

synchronized(需要锁住的对象或类){
       
       锁定的部分,需要锁才能运行。
}

 ()中可以确定锁定的是对象还是类,锁定对象的话可以用this,对类上锁类名加class,例如要锁定Mounth类(Moutn.class)。

我们首先看个没有任何同步的例子:

 1 public class TestThread {
 2      public static void main(String[] args){
 3          TestSynchronized s1 = new TestSynchronized();
 4     
 5          new Thread(s1,"t1").start();  //两个线程访问一个对象
 6          new Thread(s1,"t2").start();
 7      }
 8 }
 9 
10 class TestSynchronized implements Runnable{
11     private int ticket = 5;
12     
13            public void run(){    
14             for(int p = 0; p < 10; p++){
15                 try {
16                     Thread.sleep(1000);
17                 } catch (InterruptedException e) {
18                     // TODO Auto-generated catch block
19                     e.printStackTrace();
20                 }
21                 if(ticket >= 0){
22                     System.out.println(Thread.currentThread().getName() + " ticket:" + ticket--);
23                 }                    
24             }
25         }
26 }
运行结果:
t1 ticket:5
t2 ticket:4
t2 ticket:3
t1 ticket:2
t2 ticket:1
t1 ticket:1
t2 ticket:0
t1 ticket:-1

其中出现了-1,我们在其中一块区域加上同步形成同步块。

 1 public class TestThread {
 2      public static void main(String[] args){
 3          TestSynchronized s1 = new TestSynchronized(); //两个线程访问一个对象
 4     
 5          new Thread(s1,"t1").start();
 6          new Thread(s1,"t2").start();
 7      }
 8 }
 9 
10 class TestSynchronized implements Runnable{
11     private int ticket = 5;
12     
13            public void run(){    
14             for(int p = 0; p < 10; p++){
15                 try {
16                     Thread.sleep(1000);
17                 } catch (InterruptedException e) {
18                     // TODO Auto-generated catch block
19                     e.printStackTrace();
20                 }
21                 synchronized(this){  //此次加上同步块,这部分内容一次只有一个线程可以进入,其他内容不受约束。
22                     if(ticket >= 0){ //这里锁的是对象,这里面的内容需要对象锁才能运行。
23                         System.out.println(Thread.currentThread().getName() + " ticket:" + ticket--);
24                     }
25                 }                    
26             }
27         }
28 }
运行结果:
t2 ticket:5
t1 ticket:4
t1 ticket:3
t2 ticket:2
t1 ticket:1
t2 ticket:0

一次只能有一个线程进入同步块中,就不会出现线程读了未更新的数据或者多减一次的情况。未加synchronized修饰的其他区域不受影响,故两个线程的顺序不定。

下面我们来看一个同步块锁定的例子,效果和例三一样,只不过例三使用静态方法锁定了类,而下面这个是使用同步块锁定了类。

 1 public class TestThread {
 2     public static void main(String[] args){
 3         TestSynchronized s1 = new TestSynchronized();
 4         TestSynchronized s2 = new TestSynchronized(); //两个线程访问两个对象
 5         new Thread(s1,"t1").start();
 6         new Thread(s2,"t2").start();
 7     }
 8 }
 9 
10 class TestSynchronized implements Runnable{
11    private int ticket = 5;
12    
13    synchronized public void run(){
14        synchronized(TestSynchronized.class){  //将synchronized修饰的静态方法改成了同步块。
15             
16            for(int p = 0; p < 10; p++){
17                try {
18                    Thread.sleep(500);
19                } catch (InterruptedException e) {
20                    // TODO Auto-generated catch block
21                    e.printStackTrace();
22                }
23                if(ticket >= 0){
24                    System.out.println(Thread.currentThread().getName() + " ticket:" + ticket--);
25                }                    
26            }
27        }
28    }
29 }
运行结果:
t1 ticket:5
t1 ticket:4
t1 ticket:3
t1 ticket:2
t1 ticket:1
t1 ticket:0
t2 ticket:5
t2 ticket:4
t2 ticket:3
t2 ticket:2
t2 ticket:1
t2 ticket:0

上述代码和例三功能一样,只是锁定方法不同,这里只是做下演示。

synchronized修饰方法是一种粗颗粒的并发控制,某一时刻只有一个线程执行方法内的内容效率较低下。

synchronized同步块是一种细颗粒的并发控制,可以自行根据需求确定区域较为灵活,可以平衡下效率和安全,同时也能因选择区域不恰当而造成问题。

只要不在synchronized方法(同步块)内的其他部分都不受限制。

普通方法锁定的对象,需要获得对象锁

静态方法锁定的是类,需要获得类锁。

同步块可以确定是锁对象(this )还是锁类(xxx.class),同时也可以自行确定区域。 

猜你喜欢

转载自www.cnblogs.com/huang-changfan/p/9460073.html