Synchronized的基本使用

Synchronized的作用

简单来说:就是能够保证在同一时刻最多只有一个线程执行该段代码,已达到保证并发安全的效果。

地位:

1、Synchronized是Java的关键字,被Java语言原生支持

2、是最基本的互斥同步手段

3、是并发编程中的元老级角色,是并发编程中必学内容

不使用Synchronized带来的后果

示例:两个线程同时操作一个变量,实现不断累加的效果。

public class synchronizedTest implements Runnable {

    static int i = 0;

    @Override
    public void run() {
        System.out.println("name:" + Thread.currentThread().getName());
        for (int j = 0; j < 100000; j++) {
            i++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        synchronizedTest synchronizedTest = new synchronizedTest();
        Thread thread1 = new Thread(synchronizedTest);
        Thread thread2 = new Thread(synchronizedTest);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();

        System.out.println(i);
    }
}

这里使用join方法,是为了让一个线程进入一个等待的状态,等该线程执行完之后,再去执行下一行代码。

我们预期的结果应该是200000,但事实并非如此。执行结果:

name:Thread-1
name:Thread-0
127096

每次运行结果都会不同,显然这是不安全的。
原因:
i++看上去只有一行代码,其实包括三个步骤:

1)读取i的值

2)将i进行加1

3)最后将i的值写入到内存中

如果线程1刚执行到第二步骤,此时线程2开始读取i的值,显然线程1还没有完成对i进行加1的操作,这样两个线程对i的操作就乱套了。

Synchronized的两种用法

1、对象锁
包括方法锁(修饰普通方法,默认锁对象是this)和同步代码块锁(自己指定锁对象)

2、类锁
指的是synchronized修饰的静态方法或指定锁为class对象。类锁只能在同一时刻被一个对象拥有。

我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁互不干扰,但是每个类只有一个类锁。有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的

一、对象锁
1、this对象锁的使用
public class synchronizedTest implements Runnable {

    @Override
    public void run() {
        synchronized (this){
            System.out.println("对象锁代码,线程名字:" + Thread.currentThread().getName());
            //这里加了3秒延迟,是为了更清楚看到线程的执行顺序
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println( Thread.currentThread().getName() + "执行完毕");
        }
    }

    public static void main(String[] args) {
        synchronizedTest synchronizedTest = new synchronizedTest();
        Thread thread1 = new Thread(synchronizedTest);
        Thread thread2 = new Thread(synchronizedTest);
        thread1.start();
        thread2.start();
        while (thread1.isAlive() || thread2.isAlive()){
            //保证线程执行完毕
        }
        System.out.println("finished");
    }
}

执行结果

对象锁代码,线程名字:Thread-0
Thread-0执行完毕
对象锁代码,线程名字:Thread-1
Thread-1执行完毕
finished

加了synchronized修饰,线程是串行执行,解决了并发的问题。

2、自己创建对象锁
public class synchronizedTest implements Runnable {

    Object lock1 = new Object();
    Object lock2 = new Object();

    @Override
    public void run() {
        synchronized (lock1){
            System.out.println("我是lock1,线程名字:" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println( Thread.currentThread().getName() + ":lock1执行完毕");
        }

        synchronized (lock2){
            System.out.println("我是lock2,线程名字:" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println( Thread.currentThread().getName() + ":lock2执行完毕");
        }
    }

    public static void main(String[] args) {
        synchronizedTest synchronizedTest = new synchronizedTest();
        Thread thread1 = new Thread(synchronizedTest);
        Thread thread2 = new Thread(synchronizedTest);
        thread1.start();
        thread2.start();
        while (thread1.isAlive() || thread2.isAlive()){
            //保证线程执行完毕
        }
        System.out.println("finished");
    }
}

执行结果

我是lock1,线程名字:Thread-0
Thread-0:lock1执行完毕
我是lock2,线程名字:Thread-0
我是lock1,线程名字:Thread-1
Thread-0:lock2执行完毕
Thread-1:lock1执行完毕
我是lock2,线程名字:Thread-1
Thread-1:lock2执行完毕
finished

但Thread-0执行完第一个同步代码块的时候,同时Thread-1会立即获取该对象锁。

Thread-0:lock1执行完毕
我是lock2,线程名字:Thread-0
我是lock1,线程名字:Thread-1

这三行日志,几乎是并行执行的。

Thread-0:lock2执行完毕
Thread-1:lock1执行完毕
我是lock2,线程名字:Thread-1

几乎也是并行执行完毕的。

如果使用同一把对象锁的情况
执行结果

我是lock1,线程名字:Thread-0
Thread-0:lock1执行完毕
我是lock2,线程名字:Thread-0
Thread-0:lock2执行完毕
我是lock1,线程名字:Thread-1
Thread-1:lock1执行完毕
我是lock2,线程名字:Thread-1
Thread-1:lock2执行完毕
finished

只有当Thread-0执行完之后,才释放lock1对象锁,Thread-1开始执行。

3、对象锁修饰普通方法的情况(方法锁)
public class synchronizedTest implements Runnable {

    @Override
    public void run() {
       method();
    }

    private synchronized void method() {
        System.out.println("我是方法锁,线程名字:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println( Thread.currentThread().getName() + "执行完毕");
    }

    public static void main(String[] args) {
        synchronizedTest synchronizedTest = new synchronizedTest();
        Thread thread1 = new Thread(synchronizedTest);
        Thread thread2 = new Thread(synchronizedTest);
        thread1.start();
        thread2.start();
        while (thread1.isAlive() || thread2.isAlive()){
            //保证线程执行完毕
        }
        System.out.println("finished");
    }
}

执行结果:

我是方法锁,线程名字:Thread-0
Thread-0执行完毕
我是方法锁,线程名字:Thread-1
Thread-1执行完毕
finished

实现了串行执行的效果。

二、类锁
1、synchronized用在static修饰的方法上
public class synchronizedTest implements Runnable {

    @Override
    public void run() {
       method();
    }

    private static synchronized void method() {
        System.out.println("使用static修饰的情况,线程名字:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println( Thread.currentThread().getName() + "执行完毕");
    }

    public static void main(String[] args) {
        synchronizedTest synchronizedTest1 = new synchronizedTest();
        synchronizedTest synchronizedTest2 = new synchronizedTest();
        Thread thread1 = new Thread(synchronizedTest1);
        Thread thread2 = new Thread(synchronizedTest2);
        thread1.start();
        thread2.start();
        while (thread1.isAlive() || thread2.isAlive()){
            //保证线程执行完毕
        }
        System.out.println("finished");
    }
}

如果我们不使用static修饰方法,执行结果

不使用static修饰的情况,线程名字:Thread-0
不使用static修饰的情况,线程名字:Thread-1
Thread-1执行完毕
Thread-0执行完毕
finished

由于创建了不同的实例对象,两个线程拿到的对象锁也不相同,它们可以同时访问synchronized修饰的普通方法,两个线程并行执行,这时synchronized失去了它的作用。

使用static修饰方法,执行结果

使用static修饰的情况,线程名字:Thread-0
Thread-0执行完毕
使用static修饰的情况,线程名字:Thread-1
Thread-1执行完毕
finished

可见,两个线程实现了串行执行。

2、synchronized(*.class)代码块
public class synchronizedTest implements Runnable {

    @Override
    public void run() {
       method();
    }

    private void method() {
        synchronized (synchronizedTest.class){
            System.out.println("使用class对象的情况,线程名字:" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println( Thread.currentThread().getName() + "执行完毕");
        }

    }

    public static void main(String[] args) {
        synchronizedTest synchronizedTest1 = new synchronizedTest();
        synchronizedTest synchronizedTest2 = new synchronizedTest();
        Thread thread1 = new Thread(synchronizedTest1);
        Thread thread2 = new Thread(synchronizedTest2);
        thread1.start();
        thread2.start();
        while (thread1.isAlive() || thread2.isAlive()){
            //保证线程执行完毕
        }
        System.out.println("finished");
    }
}

执行结果

使用class对象的情况,线程名字:Thread-0
Thread-0执行完毕
使用class对象的情况,线程名字:Thread-1
Thread-1执行完毕
finished
多线程访问同步方法的7种情况

1、两个线程同时访问一个对象的同步方法
答:串行执行

2、两个线程访问的是两个对象的同步方法
答:并行执行,因为两个线程持有的是各自的对象锁,互补影响。

3、两个线程访问的是synchronized的static方法
答:串行执行,持有一个类锁

4、同时访问同步方法和非同步方法
答:并行执行,无论是同一对象还是不同对象,普通方法都不会受到影响

5、访问同一对象的不同的普通同步方法
答:串行执行,持有相同的锁对象

6、同时访问静态的synchronized方法和非静态的synchronized方法
答:并行执行,因为一个是持有的class类锁,一个是持有的是this对象锁,不同的锁,互补干扰。

7、方法抛出异常后,会释放锁
答:synchronized无论是正常结束还是抛出异常后,都会释放锁,而lock必须手动释放锁才可以。

问题3和4参考代码:

public class synchronizedTest implements Runnable {

    @Override
    public void run() {
        if (Thread.currentThread().getName().equals("Thread-0")) {
            method1();
        } else {
            method2();
        }
    }

    private synchronized void method1() {
        System.out.println("使用synchronized修饰的方法,线程名字:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "执行完毕");
    }

    private void method2() {
        System.out.println("使用普通方法,线程名字:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "执行完毕");
    }

    public static void main(String[] args) {
        synchronizedTest synchronizedTest1 = new synchronizedTest();
        Thread thread1 = new Thread(synchronizedTest1);
        Thread thread2 = new Thread(synchronizedTest1);
        thread1.start();
        thread2.start();
        while (thread1.isAlive() || thread2.isAlive()) {
            //保证线程执行完毕
        }
        System.out.println("finished");
    }
}

执行结果

使用synchronized修饰的方法,线程名字:Thread-0
使用普通方法,线程名字:Thread-1
Thread-1执行完毕
Thread-0执行完毕
finished

synchronized的缺陷

当某个线程进入同步方法获得对象锁,那么其他线程访问这里对象的同步方法时,必须等待或者阻塞,这对高并发的系统是致命的,这很容易导致系统的崩溃。如果某个线程在同步方法里面发生了死循环,那么它就永远不会释放这个对象锁,那么其他线程就要永远的等待。这是一个致命的问题。

思考:既然有了synchronized修饰方法的同步方式,为什么还需要synchronized修饰同步代码块的方式呢?

而这个问题也是synchronized的缺陷所在。当然同步方法和同步代码块都会有这样的缺陷,只要用了synchronized关键字就会有这样的风险和缺陷。既然避免不了这种缺陷,那么就应该将风险降到最低。这也是同步代码块在某种情况下要优于同步方法的方面,因为synchronized修饰同步代码块的方式比修饰方法的方式粒度小。

synchronized的性质

1、可重入性:指的是同一线程的外层函数获得锁之后,内层函数可以直接再次获取该锁。获得锁之后不需要重新获取,可直接获取,直到自己释放。

好处:避免死锁,提升封装性(如果不可重入,假设method1拿到锁之后,在method1中又调用了method2,如果method2没办法使用method1拿到的锁,那method2将一直等待,但是method1由于未执行完毕,又无法释放锁,就导致了死锁,可重入正好避免了这种情况)

粒度:线程而非调用

这里解释下粒度:指的就是锁的作用范围,锁定的粒度越小(即锁定的业务代码越少),效率越高。这里Java的关键字synchronized加锁的范围默认指的是线程。而其他不一定是,例如Linux里的pthread线程,它是以调用为粒度的。

通过以下三种情况证明可重入性

(1)同一个方法是可重入的(递归调用)

public class synchronizedTest {

    int i = 0;

    private synchronized void method1() {
        System.out.println("我是method1,i=" + i);
        if (i == 0) {
            i++;
            method1();
        }
    }

    public static void main(String[] args) {
        new synchronizedTest().method1();
    }
}

执行结果

我是method1,i=0
我是method1,i=1

(2)可重入不要求是同一个方法

public class synchronizedTest {

    private synchronized void method1() {
        System.out.println("我是method1");
        method2();
    }

    private synchronized void method2() {
        System.out.println("我是method2");

    }

    public static void main(String[] args) {
        new synchronizedTest().method1();
    }
}

执行结果

我是method1
我是method2

(3)可重入不要求是同一个类中

public class synchronizedTest {

    public void superMethod() {
        System.out.println("我是父类方法");
    }
}

class ChildClass extends synchronizedTest {

    @Override
    public void superMethod() {
        System.out.println("我是子类方法");
        super.superMethod();
    }

    public static void main(String[] args) {
        new ChildClass().superMethod();
    }
}

执行结果

我是子类方法
我是父类方法

通过以上三种情况测试,可知synchronized的粒度是线程范围,而不是调用。

2、不可中断
如果一个锁已经被一个线程获得,其他线程还想获得,只能选择等待或者阻塞,直到正在用的线程释放这个锁,其他线程才能获得,否则永远等待。(Lock可以中断)

synchronized和Lock的区别
1、Lock是java的一个interface接口,而synchronized是Java中的关键字,synchronized是由JDK实现的,不需要程序员编写代码去控制加锁和释放。

2、synchronized修饰的代码在执行异常时,jdk会自动释放线程占有的锁,不需要程序员去控制释放锁,因此不会导致死锁现象发生;但是,当Lock发生异常时,如果程序没有通过unLock()去释放锁,则很可能造成死锁现象,因此Lock一般都是在finally块中释放锁;格式如下:

Lock lock = new ReentrantLock();
        lock.lock();//加锁
        try{
            //TODO
        }catch (Exception e){
            //TODO
        }finally {
            lock.unlock();//释放锁
        }

3、Lock可以让等待锁的线程响应中断处理,如tryLock(long time, TimeUnit unit),而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够中断,程序员无法控制.

4、通过Lock可以知道有没有成功获取锁,tryLock()方法返回boolean值,因此可知道是否获得了锁,而synchronized却无法办到。

5、Lock的实现类ReentrantReadWriteLock提供了readLock()和writeLock()用来获取读锁和写锁的两个方法,这样多个线程可以进行同时读写操作。

总体来讲,当并发量较小,资源竞争不激烈时,两者的性能是差不多的;当大量线程同时竞争,资源非常有限时,此时Lock的性能要远远优于synchronized。

总结
1、synchronized修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
2、synchronized修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
3、synchronized修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
4、synchronized修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值