java synchronized机制_Java同步机制之synchronized

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();

}

上述代码的输出结果是无法确定的,下面是它的执行结果之一:

a713cdb305c2

image

代码总是无法按照我们的预期打印出40000(原因:线程安全中的原子性),如果两个线程同时操作addTest中的变量,则可能会出现线程安全性问题。我们使用synchronized对AddTest方法进行同步:

public class AddTest {

private int num = 0;

public int getNum(){

return num;

}

public synchronized void addOne(){

num++;

}

}

代码执行结果如下:

a713cdb305c2

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();

这段代码创建了两个实例,并分别在两个线程执行它们各自的方法。代码执行输出如下:

a713cdb305c2

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();

}

代码的执行结果如下:

a713cdb305c2

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));

}

}

打印他们的执行结果和时间:

a713cdb305c2

image

这个代码是非线程安全的,将addOne方法改为同步方法:

public synchronized void addOne(){

try {

Thread.sleep(10);

} catch (InterruptedException e) {

e.printStackTrace();

}

num++;

}

a713cdb305c2

image

此时线程变得安全了,但是代码执行的时间却增加了很多。

使用同步方法的策略可以简单确保线程安全,但是这种粗粒度的实现方式带来的代价是惨痛的。假如我们我个Service实现对某页面的访问量。但当我们将它设计成一个同步方法时,就使得每次只有一个线程可以访问它,这在高负载的情况下会使得程序的执行时间变得很长——因为所有的请求都必须排队执行。这完全背离了我们程序设计的初衷,而解决这个问题的方法就是同步代码块。

synchronized同步代码块

上文中讲述synchronized同步方法的弊端中,我们可以发现,在同步的时候,线程在Thread.sleep(10)时也是需要阻塞并同步执行的。而这块代码并不需要保证是安全,假如我们可以使用异步的方式进行这个等待操作,代码的执行效率就会有很大的提升。

实例锁

修改addOne方法:

public void addOne() {

try {

Thread.sleep(10);

} catch (InterruptedException e) {

e.printStackTrace();

}

synchronized (this) {

num++;

}

}

执行结果为:

a713cdb305c2

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();

代码执行结果如下:

a713cdb305c2

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();

执行结果如下:

a713cdb305c2

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++;

}

}

a713cdb305c2

image

两个线程分别访问两个不同对象实例的方法,不存在多线程访问同一个对象实例的问题,记录代码执行时间。

接下来修改一下addOne的代码,使用mObject对它的代码进行保护:

public void addOne() {

synchronized (mObject) {

try {

Thread.sleep(10);

} catch (InterruptedException e) {

e.printStackTrace();

}

num++;

}

}

执行结果如下:

a713cdb305c2

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修饰的方法时,线程一定是排队运行的

当线程执行代码时发生异常,锁会被自动释放

线程间同时访问同一个锁的多个同步代码的执行顺序不定

当一个线程进入同步方法时,其他线程可以正常访问其他非同步方法

多个对象多个锁不会存在阻塞,多个对象一个锁会存在线程阻塞

最后

水平有限,码字不易。如有纰漏,望指正!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值