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),同时也可以自行确定区域。
上一篇: 校园里飘出了爆笑声!