多线程(三) 同步synchronized
五、同步
1.锁
多线程程序一般是为了完成一些相同的工作而存在的,因此有时间也会共享一些资源,例如对象、变量等等,此时如果不对各个线程进行资源协调,就会出现一些冲突,从而导致程序功能失效。例如下面的示例中的计数器:
public class Sync extends Thread{ public int id; int count=1000; static int data=0; public Sync(int id) { this.id=id; } public void run() { int d=((id%2)==0?1:-1); for (int i=0;i<count;i++) { data+=d; } } public static void main(String []args) { Thread A=new Sync(1); Thread B=new Sync(2); A.start(); B.start(); try { Thread.sleep(30000); //main线程等一下Sync线程 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Sync.data); } }
上面代码很简单,就是两个线程共同计数,但是线程A加1,线程B减一,各做1000次,最终的结果应该是0,实际运行起来也是0。
接下来,我们将count的值改为10000000,再次运行,三次结果分别为-26137、-9578836、899,其实意思就是结果不固定而且不稳定。
此时我们必须进行一些处理使结果变得正确,即建立同步机制。JVM通过给对象加锁的方法来实现多线程的同步处理,老调重弹一下,对象分为类对象和实例对象,实例对象不用多说,就是new出来的那种,而类对象需要通过forName(String )来获得,如:Class t=Class.forName("java.lang.Thread"); 其中返回值t记为Thread的类对象,一个类的静态成员于和成员方法属于类对象,而不是实例对象。
每个对象有一把锁(lock)和一个等候集(wait set),在一个对象内部,锁住的是同步方法和同步语句块,如下图所示:
那么将方法或者代码块变成同步的方法就是synchronized关键字。
2.synchronized关键字
synchronized关键字主要有代码块和函数两种修饰方法,修饰函数时只需要直接加上关键字即可,修饰代码块时基本格式如下:
synchronized (obj)
{
代码块
}
其中obj代表实例对象或者类对象。
在java中,每一个对象有且仅有一个同步锁。这也意味着,同步锁是依赖于对象而存在。我们改造一下上面的例子,使其成为Runnable接口的形式。
/* Multi-thread Conflict - count minus two million * wym * */ public class Sync2 implements Runnable{ public int id; int count=1000000; static int data=0; public boolean b; public Sync2(int id , boolean b) { this.id=id; this.b=b; } public void run() { int d=((id%2==0)?1:-1); for (int i = 0; i < count; i++) { data += d; } b = true; System.out.println("Thread " + id + " is over"); } public boolean getb() { return b; } public static void main(String []args) { Sync2 s=new Sync2(1,false); Thread t1=new Thread(s,"t1"); Thread t2=new Thread(s,"t2"); t1.start(); t2.start(); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("end"+Sync2.data); } }
显然还是会产生冲突,因为只是从线程的继承Thread类方式变成了实现Runnable接口,运行一次的结果是-1006544(不固定),但是当我们对run()方法里面加上synchronized关键字之后:
public void run() { synchronized (this) { int d=((id%2==0)?1:-1); for (int i = 0; i < count; i++) { data += d; } b = true; System.out.println("Thread " + id + " is over"); } }
显然解决了冲突问题,无论运行多少次,结果都是-2000000。那么问题来了,为什么不能继承Thread类来实现呢?前面说到过了,同步锁是基于对象的,继承Thread类时,两个线程需要两个实例对象,而Runnable接口两个线程基于同一个实现该接口的对象的,因此要使用Runnable接口方式。
我们是否能实现基于Thread的方法呢?当然可以!
class M { static int z=0; static int count=1000000; public void count(int d) { synchronized (this) { for (int i = 0; i < count; i++) { z += d; } } } } public class Sync3 extends Thread { M m; int id; public Sync3(int id ,M m) { this.id = id; this.m=m; } public int getid() { return id; } public void run() { m.count(id); } public static void main(String[] args) { M m1=new M(); Thread t1 = new Sync3(1,m1); Thread t2 = new Sync3(-1,m1); t1.start(); t2.start(); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("end"+M.z); } }
基本思路是在线程类(继承Thread)中添加一个对象的成员域,然后两个线程的这个成员被同一个实例对象赋值,然后将冲突的方法放在该类的方法中,并将这个方法设置同步方法,即可解决第一节中的冲突,输出结果0.
如下两句话:
当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程仍然可以访问“该对象”的非同步代码块。

class M { public synchronized void syn(int id) { for (int i=0;i<5;i++) { System.out.println("Thread "+id+" syn "+i); } } public void nonsyn(int id2) { for (int i=0;i<5;i++) { System.out.println("Thread "+id2+" nonsyn "+i); } } } public class Sync3 extends Thread { M m; int id; public Sync3(int id ,M m) { this.id = id; this.m=m; } public int getid() { return id; } public void run() { if(id==1) m.syn(id); else m.nonsyn(id); } public static void main(String[] args) { M m1=new M(); Thread t1 = new Sync3(1,m1); Thread t2 = new Sync3(2,m1); t1.start(); t2.start(); } }
Thread 2 nonsyn 0 Thread 1 syn 0 Thread 2 nonsyn 1 Thread 1 syn 1 Thread 2 nonsyn 2 Thread 2 nonsyn 3 Thread 2 nonsyn 4 Thread 1 syn 2 Thread 1 syn 3 Thread 1 syn 4
其实好几次都是按照顺序来的,好不容易跑出这结果😂。
当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程对“该对象”的其他的“synchronized方法”或者“synchronized代码块”的访问将被阻塞。
代码只需要将上面的nonsyn()方法加上synchronized关键字再来看结果:
Thread 1 syn 0 Thread 1 syn 1 Thread 1 syn 2 Thread 1 syn 3 Thread 1 syn 4 Thread 2 nonsyn 0 Thread 2 nonsyn 1 Thread 2 nonsyn 2 Thread 2 nonsyn 3 Thread 2 nonsyn 4
为了避免随机情况的出现,我运行了五次,结果都是两个方法先后执行,可能具体顺序会有不同,但都是一个执行完之后再执行另一个。
3.类锁和对象锁
先谈对象锁,有两种形式,之前的代码中均出现过,一是以直接修饰方法的形式,例如:
public synchronized void syn(int id) { for (int i=0;i<5;i++) { System.out.println("Thread "+id+" syn "+i); } }
或者是synchronized代码块修饰实例对象,例如:
synchronized (this) { int d=((id%2==0)?1:-1); for (int i = 0; i < count; i++) { data += d; } b = true; System.out.println("Thread " + id + " is over"); }
实例锁只能对同一个实例对象起作用,因此对继承Thread实现多线程的方法并不友好,前面咱们也提出了一种解决方案,即声明一个对象成员域。也许还有更合适的方案,就是类锁,也有地方称其为全局锁。
类锁同样有两种形式,一种是直接对synchronized方法加上static属性,即:
public static synchronized void syn(int id)
而对代码块则是将实例对象改为类对象:
synchronized (Class.forName("ClassName"))
比如以下例子可以用Thread方式实现多线程,并使用类锁解决线程冲突。

/* Multi-thread Conflict * wym * */ public class Sync extends Thread { public int id; int count=1000000; static int data=0; public Sync(int id) { this.id=id; } public void run() { try { synchronized (Class.forName("Sync")) { int d = ((id % 2 == 0) ? 1 : -1); for (int i = 0; i < count; i++) { data += d; } System.out.println("Thread " + id + " is over"); } } catch (Exception e) { e.printStackTrace(); } } public static void main(String []args) { Sync A=new Sync(1); Sync B=new Sync(2); A.start(); B.start(); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("end"+Sync.data); } }
注意static不能修饰run()方法,因此无法直接进行修改,我们选取Sync3类为例说明:

class M { public static synchronized void syn(int id) { for (int i=0;i<20;i++) { System.out.println("Thread "+id+" syn "+i); } } public static synchronized void nonsyn(int id2) { for (int i=0;i<20;i++) { System.out.println("Thread "+id2+" nonsyn "+i); } } } public class Sync3 extends Thread { M m; int id; public Sync3(int id ,M m) { this.id = id; this.m=m; } public int getid() { return id; } public void run() { if(id==1) m.syn(id); else m.nonsyn(id); } public static void main(String[] args) { M m1=new M(); M m2=new M(); Thread t1 = new Sync3(1,m1); Thread t2 = new Sync3(2,m2); t1.start(); t2.start(); } }
可以来分析一下,这两个方法是否有静态属性的情况,如下图:
若两个方法都不是静态方法,显然两个方法在(b)和(c)中的对象锁中,因此不会产生冲突("不要在意对象名字,从别处抄来的");若两个都是静态方法,那么都会在(a)中的类锁中,会互相排斥;若其中一个是静态方法,那么一个是类锁,一个在对象锁,因此同两个都是静态方法相同,不会产生冲突,通俗来说就是会同步运行。
4.死锁问题
死锁一般出现在资源并不短缺,但程序设计不合理的情况。一个典型的例子就是几个线程都获取了若干个锁,但同时又在等待其他线程的锁,每个线程都处在阻塞态,这个问题虚拟机无法处理,只能依靠编程人员设计合理的程序去避免死锁问题。

public class Lock extends Thread{ static Object A=new Object(); static Object B=new Object(); boolean b; public Lock(boolean b) { this.b=b; } public void run() { synchronized (b ? A : B) { System.out.println("线程" + getName() + "锁住" + (b ? "A" : "B") + " 对象"); try { sleep(300); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (b ? B : A) { System.out.println("线程" + getName() + "锁住" + (b ? "B" : "A") + " 对象"); } } } public static void main(String []args) { Lock l1=new Lock(true); Lock l2=new Lock(false); l1.start(); l2.start(); } }
刚开始怎么也调不出来死锁,后来发现是两个Object对象忘记加静态属性,那么两个线程得A,B对象完全不一样,所以对象锁都不同,自然也不会产生死锁问题。
如何避免死锁?
首当其冲就是做好对线程顺序得设计,从源头上尽量避免死锁;若无法避免,有检测死锁和超时放弃两种思路,我们可以通过设计程序使一个线程请求一个锁超过一定时间后放弃申请。检测死锁则是设计出数据结构来判断当前是否出现死锁问题(可行),然后进行接下来得处理,比如全部撤回请求等待一段时间重新请求,或者是设置线程优先级,撤回其中一个或几个线程得请求。
这目前不是我得重点,所能做的还是对程序设计得要求。