概述
Java并发系列番外篇——同步机制(一)
隐式锁,又称线程同步synchronized。保证在同一时刻最多只有一个线程执行该段代码
前言
在上篇文章《线程安全性》中,提到了Java提供了一种内置的锁机制来支持原子性性,也就是使用synchronized修饰代码块或者方法:
synchronized (lock){
//被保护的代码块
}
public synchronized void method() {
被保护的方法
}
每个Java对象都可以用来作为一个同步锁,即内置锁(监视器锁)。线程在进入同步代码块之前会自动获取锁,并且在退出的时候释放锁。获得锁唯一的方法就是进入由这个锁保护的同步代码块或者方法。Synchronized是Java中解决并发问题的一种最常用最简单的方法,它可以确保线程互斥的访问同步代码。
synchronized同步方法
非线程安全问题存在于实例变量中,如果变量是方法内部的私有变量,则这个变量是安全的,不存在线程安全问题:
public void add() {
int a = 0;
if (a < 200) {
a++;
} else {
todo()
}
}
add就是一个线程安全的方法,因为它的内部变量a是私有的,而且它不持有外部变量。如果没有共享资源,就没有同步的必要。
用synchronized修饰方法
如果多个线程共同访问同一个对象中的实例变量,就有可能出现非线程安全的问题,例如下面的代码,两个线程对同一个对象中的变量各进行加一操作两万次:
public class AddTest {
private int num = 0;
public int getNum(){
return num;
}
public void addOne(){
num++;
}
}
public class ThreadAdd extends Thread {
private AddTest mAddTest;
public ThreadAdd(AddTest addTest){
this.mAddTest = addTest;
}
@Override
public void run() {
super.run();
for (int i = 0;i<20000;i++) {
mAddTest.addOne();
}
System.out.println("ThreadAdd:"+mAddTest.getNum());
}
}
public static void main(String[] args) {
AddTest addTest = new AddTest();
ThreadAdd threadAdd1 = new ThreadAdd(addTest);
ThreadAdd threadAdd2 = new ThreadAdd(addTest);
threadAdd1.start();
threadAdd2.start();
}
上述代码的输出结果是无法确定的,下面是它的执行结果之一:
image
代码总是无法按照我们的预期打印出40000(原因:线程安全中的原子性),如果两个线程同时操作addTest中的变量,则可能会出现线程安全性问题。我们使用synchronized对AddTest方法进行同步:
public class AddTest {
private int num = 0;
public int getNum(){
return num;
}
public synchronized void addOne(){
num++;
}
}
代码执行结果如下:
image
当两个线程同时对addTest的addOne方法进行操作,只有一个线程能够抢到锁。这个锁为当前的实例对象addTest,一个线程获取了该对象锁(实例锁)之后,其他线程无法获取该对象的锁,就不能访问该对象的synchronized方法,但是可以访问非synchronized修饰的方法。
上文中的代码里只有一个实例addTest,所有两个线程争夺同一把锁,但是如果有多个实例,也就是有多把锁会是什么情况呢:
我们稍微修改一下上文代码中线程创建的方式:
AddTest addTest1 = new AddTest();
AddTest addTest2 = new AddTest();
ThreadAdd threadAdd1 = new ThreadAdd(addTest1);
ThreadAdd threadAdd2 = new ThreadAdd(addTest2);
threadAdd1.start();
threadAdd2.start();
这段代码创建了两个实例,并分别在两个线程执行它们各自的方法。代码执行输出如下:
image
两个线程互不干扰(实际上他们是交替异步执行的)。当多个线程访问多个对象的,JVM会创建多个锁,每个锁只是锁着它对应的实例。不同的线程持有不同的锁,访问不同的对象。
在调用synchronized修饰的方法时,线程一定是排队运行的,只有共享资源的读写才需要同步,如果不是共享资源,根本就没有同步的必要。
接下来我们修改一下AddTest:
public class AddTest {
private static int num = 0;
public int getNum(){
return num;
}
public static synchronized void addOne(){
num++;
}
}
AddTest addTest1 = new AddTest();
AddTest addTest2 = new AddTest();
ThreadAdd threadAdd1 = new ThreadAdd(addTest1);
ThreadAdd threadAdd2 = new ThreadAdd(addTest2);
threadAdd1.start();
threadAdd2.start();
我们使用synchronized修饰静态方法,
public class AddTest {
private static int num = 0;
public int getNum(){
return num;
}
public static synchronized void addOne(String name){
num++;
System.out.println(name+":"+num);
}
}
public class ThreadAdd extends Thread {
private AddTest mAddTest;
private String name;
public ThreadAdd(AddTest addTest,String name){
this.mAddTest = addTest;
this.name = name;
}
@Override
public void run() {
super.run();
for (int i = 0;i<10;i++) {
try {
Thread.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
mAddTest.addOne(name);
}
System.out.println(name + "------:"+mAddTest.getNum());
}
}
public static void main(String[] args) {
AddTest addTest1 = new AddTest();
AddTest addTest2 = new AddTest();
ThreadAdd threadAdd1 = new ThreadAdd(addTest1,"A");
ThreadAdd threadAdd2 = new ThreadAdd(addTest2,"B");
threadAdd1.start();
threadAdd2.start();
}
代码的执行结果如下:
image
尽管第一个执行完的线程打印的结果总是不确定的,但是最后一个线程的结果总是40000。因为这两个线程持有的是同一把锁,此时它们持有的锁不再是对象锁,而是类锁,也就是Class对象锁,这把锁不管当前有多少实例存在,都确保了只有一个线程可以放完这个类。
锁重入
关键字synchronized有用锁重入的功能,在使用synchronized时,当一个线程得到一个对象锁后,再次请求次对象锁是可以再次得到锁的(自己可以再次得到自己持有的锁)。这也使得:在一个synchronized方法/代码块中调用同一把锁保护的synchronized方法/代码块是可行的:
public class AddTest {
private int num = 0;
public int getNum(){
return num;
}
public synchronized void addOne(){
num++;
cutOne();
}
public synchronized void cutOne(){
num--;
}
}
执行这段代码,获取的num值为0,在线程持有锁并执行addOne方法内部调用cutOne时,该线程并未释放锁,调用cutOne方法时,可再次获得锁。
可重入锁支持继承
public class Human {
public synchronized void method(){
}
}
public class Student extends Human {
@Override
public synchronized void method() {
//调用父类的同步方法
super.method();
}
}
子类可以通过可重入锁低啊用父类的同步方法。
当同步方法或者代码块执行完毕的时候,锁就会被释放。而当线程执行代码时发生异常,锁也会被自动释放。
虽然锁重入支持继承,但是同步不支持继承,如上文中的代码:尽管父类Human的method方法是同步方法。但是子类Student必须使用synchronized修饰method方法,才能确保该它的method方法是同步方法。
synchronized方法的弊端
观察下面的代码:
class Run {
public static void main(String[] args) {
AddTest addTest = new AddTest();
ThreadAdd threadAdd1 = new ThreadAdd(addTest,"A");
ThreadAdd threadAdd2 = new ThreadAdd(addTest,"B");
threadAdd1.start();
threadAdd2.start();
}
}
public class AddTest {
private int num = 0;
public int getNum(){
return num;
}
public void addOne(){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
num++;
}
}
public class ThreadAdd extends Thread {
private AddTest mAddTest;
private String name;
public ThreadAdd(AddTest addTest,String name){
this.mAddTest = addTest;
this.name = name;
}
@Override
public void run() {
super.run();
Long start = System.currentTimeMillis();
for (int i = 0;i<100;i++) {
mAddTest.addOne();
}
System.out.println("ThreadAdd:" + mAddTest.getNum() + name + ":" + String.valueOf(System.currentTimeMillis() - start));
}
}
打印他们的执行结果和时间:
image
这个代码是非线程安全的,将addOne方法改为同步方法:
public synchronized void addOne(){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
num++;
}
image
此时线程变得安全了,但是代码执行的时间却增加了很多。
使用同步方法的策略可以简单确保线程安全,但是这种粗粒度的实现方式带来的代价是惨痛的。假如我们我个Service实现对某页面的访问量。但当我们将它设计成一个同步方法时,就使得每次只有一个线程可以访问它,这在高负载的情况下会使得程序的执行时间变得很长——因为所有的请求都必须排队执行。这完全背离了我们程序设计的初衷,而解决这个问题的方法就是同步代码块。
synchronized同步代码块
上文中讲述synchronized同步方法的弊端中,我们可以发现,在同步的时候,线程在Thread.sleep(10)时也是需要阻塞并同步执行的。而这块代码并不需要保证是安全,假如我们可以使用异步的方式进行这个等待操作,代码的执行效率就会有很大的提升。
实例锁
修改addOne方法:
public void addOne() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (this) {
num++;
}
}
执行结果为:
image
代码的执行效率有了显著的提高,因为线程等待的不再是同步的了,addOne方法不再是同步执行的了,任何线程都可以访问该方法,只有在进行num++操作时才需要同步执行。
在使用同步synchronized (this)代码块时:当一个线程访问该对象的一个同步代码块时,其它线程对同一对象实例中任何synchronized (this)同步代码块都将被阻塞。保护这些代码的所都是同一个,也就是当前类的一个实例对象。尝试使用不同实例,修改线程执行的代码:
AddTest addTest = new AddTest();
AddTest addTest2 = new AddTest();
ThreadAdd threadAdd1 = new ThreadAdd(addTest,"A");
ThreadAdd threadAdd2 = new ThreadAdd(addTest2,"B");
threadAdd1.start();
threadAdd2.start();
代码执行结果如下:
image
两个线程的执行没有任何干扰,各自执行这自己的操作。因为两个线程分别持有不同的对象,访问了不同实例对象的addOne方法,而方法中的同步代码块也被不同的实例对象作为锁保护着。
我们可以把任何的对象作为一个锁,修改上述代码:
public class AddTest {
private int num = 0;
private Object mObject = new Object();
public int getNum() {
return num;
}
public void addOne() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (mObject) {
num++;
}
}
}
AddTest addTest = new AddTest();
ThreadAdd threadAdd1 = new ThreadAdd(addTest,"A");
ThreadAdd threadAdd2 = new ThreadAdd(addTest,"B");
threadAdd1.start();
threadAdd2.start();
执行结果如下:
image
将mObject作为一个锁保护着num++操作。
尝试一些使用同一个锁去保护不同的实例对象:
首先看下面代码的执行结果:
Object o = new Object();
AddTest addTest = new AddTest(o);
AddTest addTest2 = new AddTest(o);
ThreadAdd threadAdd1 = new ThreadAdd(addTest,"A");
ThreadAdd threadAdd2 = new ThreadAdd(addTest2,"B");
threadAdd1.start();
threadAdd2.start();
public class AddTest {
private Object mObject;
private int num = 0;
public AddTest(Object object) {
this.mObject = object;
}
public int getNum() {
return num;
}
public void addOne() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
num++;
}
}
image
两个线程分别访问两个不同对象实例的方法,不存在多线程访问同一个对象实例的问题,记录代码执行时间。
接下来修改一下addOne的代码,使用mObject对它的代码进行保护:
public void addOne() {
synchronized (mObject) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
num++;
}
}
执行结果如下:
image
仍然是各自线程访问自己各自的对象实例,但是执行时间却大幅上涨——这是因为两个线程持有同一把锁,当一个线程A持有该锁时,线程B无法访问用该锁保护的任何代码块。即使这段代码块和线程A没有任何关系,也不会被线程A访问。
Class锁
接着上述代码,继续修改addOne方法:
public void addOne() {
synchronized (AddTest.class) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
num++;
}
}
其它代码保持不变,仅仅将synchronized(mObject)用synchronized (AddTest.class)替换,会产生同样效果。因为用来保护代码块的是同一把锁——类锁。锁是加持在类上的,用synchronized static或者synchronized(class)方法使用的锁都是类锁,因为class和静态方法在系统中只会产生一份,所以在单系统环境中使用类锁是线程安全的。
类锁和上面的对象锁唯一不同的区别是,类锁只有一把,无论你创建多少实例对象,它们都公用一把锁。而对象锁你可以动态的使用不同的锁,如果你能确保所有的同步都用同一个对象锁,那么对象锁也能实现类锁的功能。
名称
描述
对象锁
synchronized 修饰非静态的方法和synchronized(this)都是使用的对象锁,一个系统可以有多个对象实例,所以使用对象锁不是线程安全的,除非保证一个系统该类型的对象只会创建一个(通常使用单例模式)才能保证线程安全;
类锁
锁是加持在类上的,用synchronized static 或者synchronized(class)方法使用的锁都是类锁,因为class和静态方法在系统中只会产生一份,所以在单系统环境中使用类锁是线程安全的;
String锁!!!
由于在JVM中具有String常量池缓存的功能,因此相同字面量是同一个锁。
总结
分类
被锁的对象
示例代码
普通方法
当前实例对象
public synchronized void method() {
}
静态方法
当前类的Class对象
public static synchronized void methodStatic() {
}
分类
被锁的对象
示例代码
普通实例对象
当前对象实例
synchronized (this){
}
类对象
当前类的Class对象
synchronized (Student.class){
}
任意对象
当前类的Class对象
String lock = new String();
synchronized (lock){
}
可重入锁支持继承
同步不具有继承性
调用synchronized修饰的方法时,线程一定是排队运行的
当线程执行代码时发生异常,锁会被自动释放
线程间同时访问同一个锁的多个同步代码的执行顺序不定
当一个线程进入同步方法时,其他线程可以正常访问其他非同步方法
多个对象多个锁不会存在阻塞,多个对象一个锁会存在线程阻塞
最后
水平有限,码字不易。如有纰漏,望指正!
最后
以上就是紧张蜻蜓为你收集整理的java synchronized机制_Java同步机制之synchronized的全部内容,希望文章能够帮你解决java synchronized机制_Java同步机制之synchronized所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复