一、多个线程操作一个对象实例
当两个线程同时访问一个没有同步的方法,如果两个线程同时操作业务对象中的实例变量,则有可能会出现“非线程安全问题”。
1 package concurrent; 2 /** 3 * 测试不同线程操作同一个实例变量线程安全问题 4 * @author foolishbird_lmy 5 * 6 */ 7 class ThePrivateNumber{ 8 private int num = 0; 9 10 public synchronized void addI(String username){ 11 try { 12 if (username.equals("a" )){ 13 num = 100; 14 System. out .println("a set over!"); 15 Thread. sleep(2000); 16 } else { 17 num = 200; 18 System. out .println("b set over!"); 19 } 20 System. out .println(Thread.currentThread().getName()+ " num is " +num ); 21 } catch (InterruptedException e){ 22 e.printStackTrace(); 23 } 24 } 25 } 26 27 class ThePrivateNumA extends Thread{ 28 private ThePrivateNumber tNumber; 29 public ThePrivateNumA(ThePrivateNumber tNumber){ 30 this .tNumber = tNumber; 31 } 32 public void run(){ 33 tNumber.addI( "a" ); 34 } 35 } 36 class ThePrivateNumB extends Thread{ 37 private ThePrivateNumber tNumber; 38 public ThePrivateNumB(ThePrivateNumber tNumber){ 39 this .tNumber = tNumber; 40 } 41 public void run(){ 42 tNumber.addI( "b" ); 43 } 44 } 45 public class ThePrivateNum { 46 public static void main(String[] args) { 47 ThePrivateNumber tNumber = new ThePrivateNumber(); 48 ThePrivateNumA tNumA = new ThePrivateNumA(tNumber); 49 tNumA.setName( "A" ); 50 tNumA.start(); 51 ThePrivateNumB tNumB = new ThePrivateNumB(tNumber); 52 tNumB.setName( "B" ); 53 tNumB.start(); 54 } 55 }
上面演示的就是两个线程操作同一个对象实例,如果没有加synchronized关键字执行同步,则会出现线程安全问题,也就是产生脏读数据,即读取到的数据是已经被修改过的数据;
分析:当线程A进入获取到CPU执行权,匹配到“a”,则num=100,之后该线程被休眠2秒,此时B线程获得CPU执行权,开始执行到else代码,执行num=200,之后退出,A线程经过短暂休眠后自动苏醒继续执行,但是此时num已经被更改为了200,所以最后输出num都是200,这就是线程非安全的;加了同步锁之后,线程B必须等待线程A执行完之后才能进入,所以不会产生数据被修改问题。
二、一个对象实例中有同步方法与非同步方法
1 package concurrent; 2 3 class MySynchroized{ 4 public synchronized void methodA(){ 5 try { 6 System. out .println("the methodA is start:"+Thread. currentThread().getName()); 7 Thread. sleep(5000); 8 System. out .println("A end time "+System.currentTimeMillis ()); 9 } catch (InterruptedException e){ 10 e.printStackTrace(); 11 } 12 } 13 //synchronized,分别测试加同步锁与不加执行顺序 14 public void methodB(){ 15 try { 16 System. out .println("the methodB is start:"+Thread. currentThread().getName()); 17 System. out .println("B begin time "+System.currentTimeMillis ()); 18 Thread. sleep(5000); 19 } catch (InterruptedException e){ 20 e.printStackTrace(); 21 } 22 } 23 } 24 class TestA extends Thread{ 25 private MySynchroized ms; 26 public TestA(MySynchroized ms){ 27 this .ms = ms; 28 } 29 public void run(){ 30 ms.methodA(); //调用同步方法 31 } 32 } 33 class TestB extends Thread{ 34 private MySynchroized ms; 35 public TestB(MySynchroized ms){ 36 this .ms = ms; 37 } 38 public void run(){ 39 ms.methodB(); //调用非同步方法 40 } 41 } 42 public class TestSynchroized { 43 public static void main(String[] args) { 44 MySynchroized ms = new MySynchroized(); 45 TestA a = new TestA(ms); 46 a.setName( "A" ); 47 TestB b = new TestB(ms); 48 b.setName( "B" ); 49 a.start(); 50 b.start(); 51 } 52 }
分析:在共享的对象实例类中有两个方法,我们分别设置为同步与非同步,
(1)左边是B方法非同步测试效果,我们可以看到,当执行A线程时,A拿到该对象的实例锁,但是并没有影响线程B执行非同步的方法,说明A、B线程几乎是同时进行,B线程并没有因为A拿到锁而发生等待现象,属于异步执行;
(2)右边是将B方法也执行同步,可以看到A、B线程同步执行即顺序执行,当A先进入拿到对象锁,B此时就会是同步等待状态,只有当A执行休眠完成,释放锁之后B才有机会执行,属于同步执行操作。
结论:对于执行相同对象的不同线程,执行对象类中的同步方法时,不管有多少个同步的方法,都是同一个对象锁,必须等一个线程执行完毕释放锁之后另一个线程才能获取执行;但是非同步方法可以任意时刻调用,不收锁限制。
三、关于脏读问题
1 package concurrent; 2 class DirtyRead { 3 private String name = "a" ; 4 private String id = "aa" ; 5 public synchronized void set(String name, String id) { 6 try { 7 this .name = name; 8 Thread. sleep(2000); 9 this .id = id; 10 System. out .println("set method " + Thread.currentThread().getName() 11 + " name:" + name + " id:" + id); 12 } catch (InterruptedException e) { 13 e.printStackTrace(); 14 } 15 } 16 public void get() { 17 System. out .println("get method " + Thread.currentThread().getName() 18 + " name:" + name + " id:" + id ); 19 } 20 } 21 class DirtyReadRun extends Thread { 22 private DirtyRead dr; 23 public DirtyReadRun(DirtyRead dr) { 24 this .dr = dr; 25 } 26 public void run() { 27 dr.set( "b" , "bb" ); 28 } 29 } 30 public class TestDirtyRead { 31 public static void main(String[] args) { 32 try { 33 DirtyRead dr = new DirtyRead(); 34 DirtyReadRun drr = new DirtyReadRun(dr); 35 drr.start(); 36 // Thread.sleep(1000); 37 Thread. sleep(3000); 38 dr.get(); 39 } catch (InterruptedException e) { 40 e.printStackTrace(); 41 } 42 } 43 }
分析:
(1)左边结果是在主线程中休眠1秒,可以发现读取数据发生了错误,原因是我们将set方法设置为同步,所以线程在执行时拿到锁后会安全执行,数据的设置没有问题,但是当我们调用非同步方法get获取值时,注意主线程只休眠了1秒,但是我们在set线程中休眠了2秒,所以此时线程并没有来得及给id赋值操作,就直接输出了aa而不是bb;
(2)右边是主线程休眠3秒的情况,即调用get方法的线程是在线程赋值set休眠2秒之后才调用,此时已经安全给数据赋值了,所以输出结果正确;
当然我们也可以直接给get方法执行同步操作,这有另一个线程就必须等待第一个线程执行完set里面的全部操作释放锁之后才能执行,会发生等待。
四、锁的可重入
关键字synchronized拥有可重入的功能,即当一个线程得到一个对象锁之后,再次请求此对象锁时是可以再次得到该对象的锁的,自己可以再次获取自己的内部锁。
1 package concurrent; 2 /** 3 * 可重入锁测试 4 * @author foolishbird_lmy 5 * 6 */ 7 class Synch{ 8 public synchronized void sA(){ 9 System. out .println("sA()" ); 10 sB(); 11 } 12 public synchronized void sB(){ 13 System. out .println("sB()" ); 14 sC(); 15 } 16 public synchronized void sC(){ 17 System. out .println("sC()" ); 18 } 19 } 20 class SynchARun extends Thread{ 21 private Synch sa; 22 public SynchARun(Synch sa){ 23 this .sa = sa; 24 } 25 public void run(){ 26 sa.sA(); 27 } 28 } 29 public class ReSynchronized { 30 public static void main(String[] args) { 31 SynchARun sa = new SynchARun( new Synch()); 32 sa.start(); 33 } 34 }
从输出结果可以看出,线程只要获取到了该对象锁,其他的同步锁也一样能获取。
五、静态同步synchronized方法与synchronized(class)代码块
用static修饰的同步方法中的同步锁是给Class类上锁,而非static方法是给对象上锁;
1 package concurrent; 2 3 class StaticSyn { 4 public static synchronized void printA() { 5 try { 6 System. out .println(Thread.currentThread().getName() + " : " 7 + System.currentTimeMillis() + "进入printA()"); 8 Thread. sleep(3000); 9 System. out .println(Thread.currentThread().getName() + " : " 10 + System.currentTimeMillis() + "退出printA()"); 11 } catch (InterruptedException e) { 12 e.printStackTrace(); 13 } 14 } 15 public static synchronized void printB() { 16 try { 17 System. out .println(Thread.currentThread().getName() + " : " 18 + System.currentTimeMillis() + "进入printB()"); 19 Thread. sleep(3000); 20 System. out .println(Thread.currentThread().getName() + " : " 21 + System.currentTimeMillis() + "退出printB()"); 22 } catch (InterruptedException e) { 23 e.printStackTrace(); 24 } 25 } 26 public synchronized void printC() { 27 try { 28 System. out .println(Thread.currentThread().getName() + " : " 29 + System.currentTimeMillis() + "进入printC()"); 30 Thread. sleep(3000); 31 System. out .println(Thread.currentThread().getName() + " : " 32 + System.currentTimeMillis() + "退出printC()"); 33 } catch (InterruptedException e) { 34 e.printStackTrace(); 35 } 36 } 37 } 38 class StaticSynRunA extends Thread{ 39 private StaticSyn ss; 40 public StaticSynRunA(StaticSyn ss){ 41 this .ss = ss; 42 } 43 @SuppressWarnings( "static-access" ) 44 public void run(){ 45 ss. printA(); 46 } 47 } 48 class StaticSynRunB extends Thread{ 49 private StaticSyn ss; 50 public StaticSynRunB(StaticSyn ss){ 51 this .ss = ss; 52 } 53 public void run(){ 54 ss .printB (); 55 } 56 } 57 class StaticSynRunC extends Thread{ 58 private StaticSyn ss; 59 public StaticSynRunC(StaticSyn ss){ 60 this .ss = ss; 61 } 62 public void run(){ 63 ss.printC(); 64 } 65 } 66 public class TestStaticSyn { 67 public static void main(String[] args) { 68 StaticSyn ss = new StaticSyn(); 69 StaticSynRunA ssa = new StaticSynRunA(ss); 70 ssa.setName( "A" ); 71 ssa.start(); 72 StaticSynRunB ssb = new StaticSynRunB(ss); 73 ssb.setName( "B" ); 74 ssb.start(); 75 StaticSynRunC ssc = new StaticSynRunC(ss); 76 ssc.setName( "C" ); 77 // ssc.start(); 78 } 79 }
分析:
(1)左边结果是注释掉C,即A、B线程都是调用静态的同步方法,所以都是同步顺序执行,他们的锁都是Class锁,是同一种锁,所以B线程必须等待A线程释放锁;
(2)右边的结果是A、B、C线程同时运行,由于C线程调用的是非静态的同步方法,非静态的同步方法是对象锁,与其他两个线程的锁不一样,所以是异步的,但是A与B还是同步执行。