Java内置锁 synchronized

本文详细介绍了Java的内置锁synchronized,包括同步代码块的组成、重入特性、类对象锁以及注意事项。通过示例解释了锁的对象是对象本身,而非引用,强调了锁的互斥性和可重入性,并提醒开发者避免使用字符串常量作为锁对象。

synchronized 同步代码块组成

      在上一篇文章: 线程安全性的基本概念中,讲到过当多个线程访问状态变量,并且至少有一个线程会修改变量的值时,就需要采用同步机制来协调这些线程对对象的访问。这里说的”同步机制“包括:synchronized关键字、volatile变量、显式锁、ThreadLoacl变量以及原子变量。
      今天梳理一下synchronized 关键字,也就是Java的内置锁-同步代码块。
同步代码块由2部分组成:一个作为锁的对象引用,一个作为由这个锁保护的代码块。synchronized修饰的方法就是一个横跨整个方法体的代码块,其中该同步代码块的锁就是方法调用所在的对象。静态的synchronized 方法的锁是Class对象

synchronized (lock) {
	// 锁保护的代码块
}

      每个Java对象都可以作为一个实现同步的锁。每个线程在进入同步代码块之前会自动获得锁,同时在退出代码块或方法时释放锁,而且不论是正常执行退出还是抛出异常退出,都会释放锁。获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。
      内置锁相当于一个互斥锁,这意味着最多只有一个线程能持有这个锁,当线程A持有某内置锁时,线程B也需要获得该锁,线程B则必须等待或者阻塞,直到线程A释放这个锁,否则B将永远等待下去。
示例:

public class MainTest {
    private Integer count = 1;

    public void test(){
        synchronized (this) {
            count++;
            ThreadUtil.sleep(1);
            System.out.println(count);
        }
    }
 	public synchronized void test1(){
        count++;
        ThreadUtil.sleep(1);
        System.out.println(count);
    }
    
	public void test2(){
        synchronized (count) {
            System.out.println(count);
        }
    }
    
 	public synchronized static void test3(){
        System.out.println("hello world");
    }
 }
 // test 和 test1 方法效果是一致的,test里的同步代码块也就是整个方法体。

重入

当一个线程请求一个其它线程持有的锁时,该线程会阻塞。由于内置锁是可重入的,当一个线程试图获取一个它已经持有的锁时,是可以成功的。重入意味着获取锁的粒度是“线程”,而不是“调用”。重入的一种方法是为每一个锁关联一个获取计数器以及所有者线程。当计数器为0时,表示这个锁未被任何线程持有,一个线程获取一个未被持有的锁时,锁的获取计数器置为1,并表示所有者线程,当线程再次获取这个锁时,计数器递增。线程退出同步代码块时,计数器递减。当计数器为0时,锁被释放。

理解以及示例

上面是《Java并发编程实战》里的一段话,怎么理解呢。
第一部分,锁的对象引用。锁跟对象是关联的,内置锁是指对象的内置锁,每个对象都可以有一个内置锁。谁需要获得这个锁?线程!当一个线程需要访问锁保护的代码块的时候,它需要先获得这个锁。这也就意味着,内置锁只有在至少2个线程的时候才有意义,如果自始至终都是一个线程在执行代码,又如何证明锁的存在呢?内置锁存在的意义就在于互斥性:只有一个线程能持有锁,其余的线程如果也要获取锁,只能等待或阻塞!
2个重点:
一:对象!锁是对象的内置锁。那么如果一个类里有多个synchronized修饰的方法呢。第一个线程访问方法A,取得了内置锁。第二个线程再去调用方法B(A,B两个方法都被synchronized修饰),线程会等待么?按这个理解是需要等待的。一个方法里有多个synchronized代码块呢?访问父类的同名方法呢?
二、线程,只有多个线程才存在因为内置锁的互斥性而产生阻塞,是线程来获取锁。

synchronized 示例:

public class MainTest {

    private StringBuilder stringBuilder = new StringBuilder();

    public synchronized void doSomething(){
        System.out.println("doSomething");
        ThreadUtil.sleep(5000L);
    }

    public synchronized void doAnothing(){
        System.out.println("doAnothing");
        ThreadUtil.sleep(2000L);
    }

    public synchronized void invokeDoSomething(){
        System.out.println("invoke doSomething method");
        doSomething();
    }
    
	public void lockOther(){
	        synchronized (stringBuilder) {
	            System.out.println("lock stringBuilder");
	        }
	    }
	    
    public static void main(String[] args) throws Exception{
        MainTest mainTest = new MainTest();

        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                mainTest.doSomething();
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                mainTest.doAnothing();
            }
        });
        Thread thread3 = new Thread(new Runnable() {
            @Override
            public void run() {
                mainTest.invokeDoSomething();
            }
        });
		Thread thread4 = new Thread(new Runnable() {
		            @Override
		            public void run() {
		                mainTest.lockOther();
		            }
		});
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}
执行结果: 
doSomething
lock stringBuilder
invoke doSomething method
doSomething
doAnothing

输出结果中:第一个doSomething和lock stringBuilder很快就出来了,等了5秒之后输出 invoke doSomething method 和 doSomething 这2行,再等5秒中才输出doAnothing。
说明thread1运行之后,thread2 和 thread3阻塞,但是thread4没有阻塞,它也立即执行。直到thread1执行完毕释放锁,thread3开始执行,thread2继续阻塞,thread3执行完毕释放锁,thread2开始执行。

如果将 invokeDoSomething 方法前面的synchronized 修饰符去掉。再次执行、结果为

doSomething
invoke doSomething method
lock stringBuilder
doSomething
doAnothing

前3行几乎是同时出现,等待5秒钟之后输出doSomething,再等待5秒钟接着输出doAnothing。
thread1执行之后,invokeDoSomething 方法立刻执行,执行了System.out.println(“invoke doSomething method”); 这句话,但是在执行doSomething() 这一句时,它必须等待上一个线程也就是thread1释放锁。

从上面的例子我们可以看到,thread1,thread2, thread3都是竞争mainTest对象的锁。而thread4获取的是stringBuilder这个对象的内置锁。
内置锁是跟对象关联的,多个线程获取同一个对象的内置锁,则需要等待,符合内置锁的互斥性。但是获取同一个类里的不同的锁, 是相互不影响的。当一个非synchronized的方法调用另一个synchronized修饰的同步方法时,只有这一部分调用代码会受到synchronized的互斥性,其余部分则照常执行。

重入示例

public class Widget {
    public synchronized void doSomething(){
        System.out.println("parent do Something start");
        ThreadUtil.sleep(3000L);
        System.out.println("parent do Something end");
    }
}

public class LoggingWidget extends Widget  {
    @Override
    public synchronized void doSomething(){
        System.out.println("calling super.doSomething");
        // 如果内置锁不是可重入的,则会死锁
        super.doSomething();
    }
}

LoggingWidget 是 Widget 的子类,并且重写了父类中的doSomething方法。如果锁不是可重入的,则会死锁。LoggingWidget访问doSomething时,需要获取对象的锁,在调用super.doSomething()时,也要获取对象的锁。如果锁不是可重入的,则在第二次获取锁时会一致阻塞,它在请求一个已经被持有的锁。
这里涉及到内置锁以及父类子类的重写,情况就比较复杂了。父类里的方法可以是synchronized 和非 synchronized的,子类里的方法也可以synchronized 和 非 synchronized的,就产生了4种组合。

// 父类,thread1,thread2,thread3都是synchronized方法, thread4是非synchronized的
public class Widget {
    public synchronized void thread1(){
        System.out.println(Thread.currentThread().getName()+": parent do thread1 start");
        ThreadUtil.sleep(3000L);
        System.out.println(Thread.currentThread().getName()+": parent do thread1 end");
    }


    public synchronized void thread2(){
        System.out.println(Thread.currentThread().getName()+": parent do thread2");
    }

    public synchronized void thread3(){
        System.out.println(Thread.currentThread().getName()+": parent do thread3");
    }

    public void thread4(){
        System.out.println(Thread.currentThread().getName()+": parent do thread4");
    }
}

public class LoggingWidget extends Widget  {
    // 重写父类的synchronized方法,并且自己也是synchronized的
    @Override
    public synchronized void thread1(){
        System.out.println("child thread1");
        System.out.println("invoke super.thread1");

        super.thread1();
    }

   //  重写父类的synchronized方法,但是自己并不是synchronized的
    @Override
    public void thread2(){
        System.out.println("child thread2 and finish");
    }

    // 重写父类的synchronized方法,自己并不是synchronized的,但是方法里调用父类的synchronized方法
    @Override
    public void thread3(){
        System.out.println("child thread3, invoke super.thread3");
        super.thread3();
    }

    // 不重写父类方法,而是直接调用父类的synchronized方法
    public void invokeParentThread2(){
        System.out.println("child revokeParentThread2, invoke super.thread2");
        super.thread2();
    }

    @Override
    public synchronized void thread4(){
        System.out.println("child thread4, invoke super.thread4");
        super.thread4();
    }
}



public class MainTest {

    public static void main(String[] args) {
        LoggingWidget widget = new LoggingWidget();
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                widget.thread1();
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                widget.thread2();
            }
        });

        Thread thread3 = new Thread(new Runnable() {
            @Override
            public void run() {
                widget.thread3();
            }
        });

        Thread thread4 = new Thread(new Runnable() {
            @Override
            public void run() {
                widget.thread4();
            }
        });
        Thread thread5 = new Thread(new Runnable() {
            @Override
            public void run() {
                widget.invokeParentThread2();
            }
        });
        /**
         * 第一个执行的线程,是synchronized方法
         */
        thread1.start();
        ThreadUtil.sleep(100);
        /**
         * thread2方法并非synchronized方法,也没有调用父类的synchronized方法,两者一起执行,无需等待
         */
        thread2.start();
        /**
         * 子类的thread3是非synchronized方法,但是调用父类的synchronized方法,执行父类方法之前的代码无需等待
         * 但是执行父类的synchronized方法时,也需要获得锁,会阻塞
         */
        thread3.start();
        /**
         * 子类的thread4方法是synchronized方法,即使父类的thread4是非synchronized的,它整个方法是需要等待的
         */
        thread4.start();
        /**
         * 直接调用父类的thread2方法(synchronized方法),调用之前的代码无需等待,但是执行父类thread2方法需要等待
         */
        thread5.start();
    }

}

执行结果
child thread1
invoke super.thread1
Thread-0: parent do thread1 start
child thread2 and finish
child thread3, invoke super.thread3
child revokeParentThread2, invoke super.thread2
// 上面6行是很快输出的, 没有等待。下面5行等待了3秒才出现
Thread-0: parent do thread1 end
child thread4, invoke super.thread4
Thread-3: parent do thread4
Thread-4: parent do thread2
Thread-2: parent do thread3

对于内置锁的可重入,网上有很多说法。例如: 执行子类synchronized方法也会获得父类的锁;只要子类和父类方法中有一个是synchronized的,则方法是synchronized的,令人糊涂。
其实,完全可以抛开子类、父类的圈圈绕绕。因为锁是对象的内置锁,你只要看对象的这个方法有没有加synchronized,没有加,那就无需等待,直接执行;如果方法里调用了super.method,那就看调用的父类方法上有没有加synchronized,加了则这部分代码需要等待锁的释放(锁是对象的锁,不是什么子类,父类的锁),没有加则无需等待;方法没有加synchronized,也没有调用父类的synchronized方法,那整个方法都是不需要同步的。
归根到底,看调的方法有没有加synchronized,不管这个方法是子类重写的还是继承自父类的(子类非synchronized方法重写父类的synchronized方法,方法也是非synchronized的),这个方法自身没有加synchronized则不需要同步,这个方法调用了另外的synchronized方法,则之前的部分不需要同步,执行synchronized方法的那一部分需要同步。

class对象锁

特别地,每个类也会有一个锁,静态的 synchronized方法 就是以Class对象作为锁。另外,它可以用来控制对 static 数据成员 (static 数据成员不专属于任何一个对象,是类成员) 的并发访问。并且,如果一个线程执行一个对象的非static synchronized 方法,另外一个线程需要执行这个对象所属类的 static synchronized 方法,也不会发生互斥现象。因为访问 static synchronized 方法占用的是class的锁,而访问非 static synchronized 方法占用的是对象锁,所以不存在互斥现象

注意事项

异常会自动释放内置锁

对于 synchronized方法 或者 synchronized代码块,当出现异常时,JVM会自动释放当前线程占用的锁,因此不会由于异常导致出现死锁现象

内置锁与字符串常量

由于字符串常量池的原因,在大多数情况下,同步synchronized代码块 都不使用 String 作为锁对象,而改用其它的对象,例如new Object()创建一个对象,因为它不会被放入缓存池中。

class Service {
    public void print(String stringParam) {
        try {
            synchronized (stringParam) {
                while (true) {
                    System.out.println(Thread.currentThread().getName());
                    Thread.sleep(1000);
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

//线程A
class ThreadA extends Thread {
    private Service service;

    public ThreadA(Service service) {
        super();
        this.service = service;
    }

    @Override
    public void run() {
        service.print("AA");
    }
}

//线程B
class ThreadB extends Thread {
    private Service service;

    public ThreadB(Service service) {
        super();
        this.service = service;
    }

    @Override
    public void run() {
        service.print("AA");
    }
}
public class MainTest {

    //测试
    public static void main(String[] args) {
        //临界资源
        Service service = new Service();

        //创建并启动线程A
        ThreadA a = new ThreadA(service);
        a.setName("A");
        a.start();

        //创建并启动线程B
        ThreadB b = new ThreadB(service);
        b.setName("B");
        b.start();
    }

线程B会永远不会执行,线程A持有了“AA"对象的锁,并且永不释放,线程B也是需要获取”AA“的锁,因为常量池的存在,他们俩获取的锁是同一个锁,导致线程B无限等待。

锁定的是对象,而不是引用

在将任何数据类型作为同步锁时,需要注意的是,是否有多个线程将同时去竞争该锁对象:
  1).若它们将同时竞争同一把锁,则这些线程之间就是同步的;
  2).否则,这些线程之间就是异步的
例如:

//资源类
class Service {
    private Object lockObj;

    public Service(Object lockObj) {
        this.lockObj = lockObj;
    }
    public void print() {
        try {
            synchronized (lockObj) {
                int x = 0;
                while (true) {
                    if (x == 0) {
                        lockObj = new Object();
                        x++;
                    }

                    System.out.println(Thread.currentThread().getName());
                    Thread.sleep(2000);
                }

            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

//线程A
class ThreadA extends Thread {
    private Service service;

    public ThreadA(Service service) {
        super();
        this.service = service;
    }

    @Override
    public void run() {
        service.print();
    }
}

//线程B
class ThreadB extends Thread {
    private Service service;

    public ThreadB(Service service) {
        super();
        this.service = service;
    }

    @Override
    public void run() {
        service.print();
    }
}
public class MainTest {

    //测试
    public static void main(String[] args) {
        //临界资源
        Object o = new Object();
        Service service = new Service(o);

        //创建并启动线程A
        ThreadA a = new ThreadA(service);
        a.setName("A");
        a.start();
		ThreadUtil.sleep(100);
        //创建并启动线程B
        ThreadB b = new ThreadB(service);
        b.setName("B");
        b.start();
    }
}

A,B两个线程没有进行同步,因为B执行的时候,虽然都是lockObj这一个引用,但已经是另一个对象了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值