synchronized的引入
当存在多个线程操作共享数据时,需要保证同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再进行,这种方式称互斥锁,也就是说当一个共享数据被当前正在访问的线程加上互斥锁后,在同一个时刻,其他线程只能处于等待的状态,直到当前线程处理完毕释放该锁。在 Java 中,关键字 synchronized可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作),同时我们还应该注意到synchronized另外一个重要的作用,synchronized可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性,完全可以替代Volatile功能)
synchronized的3种应用方式
synchronized关键字最主要有以下3种应用方式,下面分别介绍
修饰实例方法,给调用该方法的对象加锁,进入同步代码前要获得当前对象的锁(this)
修饰静态方法,给当前类对象加锁,进入同步代码前要获得当前类对象的锁(Class对象)
修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
synchronized修饰非静态方法
public class AccountingSync implements Runnable{ //共享资源(临界资源) static int i=0; /** * synchronized 修饰非静态方法 */ public synchronized void increase(){ i++; } @Override public void run() { for(int j=0;j<1000000;j++){ increase(); } } public static void main(String[] args) throws InterruptedException { AccountingSync instance=new AccountingSync(); Thread t1=new Thread(instance); Thread t2=new Thread(instance); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); } /** * 输出结果: * 2000000 */ }
同一个对象在两个线程中分别访问该对象的同步方法,第一个线程获取到到对象锁并访问该同步方法的,第二个线程因无法拿到该对象锁而无法访问该同步方法,需要等待第一个线程执行结束并释放锁后才能访问,这样形成互斥!
说明:非静态方法的锁是该对象
如果第一个线程访问A对象的同步方法,第二个线程访问B对象的同步方法,这样不会引起互斥。如下
public static void main(String[] args) throws InterruptedException { //new新实例 Thread t1=new Thread(new AccountingSyncBad()); //new新实例 Thread t2=new Thread(new AccountingSyncBad()); t1.start(); t2.start(); //join含义:当前线程A等待thread线程终止之后才能从thread.join()返回 t1.join(); t2.join(); System.out.println(i); }
第一个线程和第二个线程分别拿到自己new出来的的对象锁,同步方法各自有各自的锁,不会引起互斥。
synchronized修饰静态方法
public class AccountingSyncClass implements Runnable{ static int i=0; /** * 作用于静态方法,锁是当前class对象,也就是 * AccountingSyncClass类对应的class对象 */ public static synchronized void increase(){ i++; } /** * 非静态,访问时锁不一样不会发生互斥 */ public synchronized void increase2(){ i++; } @Override public void run() { for(int j=0;j<1000000;j++){ increase(); } } public static void main(String[] args) throws InterruptedException { Thread t1=new Thread(new AccountingSyncClass()); Thread t2=new Thread(new AccountingSyncClass()); //启动线程 t1.start();t2.start(); t1.join();t2.join(); System.out.println(i); } }
上面的两个线程虽然各自拥有不同的对象,但是访问的是静态方法,静态方法的锁是当前类对象,两个线程访问同步静态方法,拿到的是当前类对象的锁,只有一个,任然是互斥的!
synchronized修饰代码块
在某些情况下,我们编写的方法体可能比较大,同时存在一些比较耗时的操作,而需要同步的代码又只有一小部分,如果直接对整个方法进行同步操作,可能会得不偿失,此时我们可以使用同步代码块的方式对需要同步的代码进行包裹,这样就无需对整个方法进行同步操作了
public class AccountingSync implements Runnable{ static AccountingSync instance=new AccountingSync(); static int i=0; @Override public void run() { //省略其他耗时操作.... //使用同步代码块对变量i进行同步操作,锁对象为instance synchronized(instance){ for(int j=0;j<1000000;j++){ i++; } } } public static void main(String[] args) throws InterruptedException { Thread t1=new Thread(instance); Thread t2=new Thread(instance); t1.start();t2.start(); t1.join();t2.join(); System.out.println(i); } }synchronized修饰代码块的时候需要指定锁对象,多线程访问的时候需要拿到这个锁对象后才能访问该静态代码块, 如果当前有其他线程正持有该对象锁,那么新到的线程就必须等待,这样也就保证了每次只有一个线程执行。通常我们经常使用this和xx.class对象来作为锁对象,
//this,当前实例对象锁 synchronized(this){ for(int j=0;j<1000000;j++){ i++; } } //class对象锁 synchronized(AccountingSync.class){ for(int j=0;j<1000000;j++){ i++; } }
同步方法和同步代码块的意义是一样的,同步方法不需要显式的指定锁对象,而代码块则需要显式指定,效果和意义是一样的。
总结:
多线程访问同步方法,能不能同时访问,主要看线程拿到的锁对象是不是唯一,如果锁对象是唯一的,意思就是多个线程会互斥访问!
1、调用同一个对象中非静态同步方法的线程是互斥的。如果是不同对象,则每个线程有自己的对象的锁,线程间彼此互不干预。
2、调用同一个类中的静态同步方法的线程将是互斥的,它们都是锁定在相同的Class对象上。
3、静态同步方法和非静态同步方法将永远不是互斥的,因为静态方法锁定在Class对象上,非静态方法锁定在该类的对象上。
4、对于同步代码块,要看清楚什么对象已经用于锁定(synchronized后面括号的内容)。在同一个对象上进行同步的线程将是互斥的,在不同对象上锁定的线程将永远不会互斥。