欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2022-06-22 11:15:06
一、为什么要用synchronized关键字 首先多线程中多个线程运行面临共享数据同步的问题。 多线程正常使用共享数据时需要经过以下步骤: 1.线程A从共享数据区中复制出数据副本,然后处理。 2.线程A将处理好的数据副本写入共享数据区。 3.线程B从共享数据区中复制出数据副本。 如此循环,直到线程结 ......

一、为什么要用synchronized关键字

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

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

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

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

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

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

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

 

 假如线程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),同时也可以自行确定区域。