美文网首页
synchronized关键字(一)

synchronized关键字(一)

作者: AD刘涛 | 来源:发表于2020-03-13 16:53 被阅读0次

synchronized的简介:

在java中synchronized关键字是同步锁,同步锁是依赖于对象而存在的,而且每一个对象有且仅有一个同步锁。当我们调用某对象的synchronized方法时,就获取了该对象的同步锁。例如,synchronized(obj)就获取了obj 这个对象的同步锁。

synchronized的原理:

Synchronized是通过对象内部的一个叫做monitor的锁来实现的,但是monitor锁的本质又是依赖于底层的操作系统的互斥锁Mutex Lock)来实现的,而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。这种依赖于操作系统互斥锁Mutex Lock)所实现的锁我们称之为重量级锁

不使用并发手段会有什么后果?

package synchronize;

/**
 * 消失的请求数
 */
public class DisappearRequest1 implements Runnable{

    static int i = 0;
    static DisappearRequest1 instance = new DisappearRequest1();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        // 同时启动线程t1与线程t2,然后调用run方法
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }

    @Override
    public void run() {
        for (int j = 0; j < 100000; j++) {
            i++;
        }
    }
}

通过运行以上代码,我们发现:两个线程同时a++操作,最后结果会比预计的少。

原因:
count++, 看上去只是一个操作, 实际上包含了三个操作,它并非原子性操作:

  1. 读取count
  2. count 加 1
  3. count的值写入内存中

在多线程环境下,任何一步执行完都有可能被打断, 都有可能轮到第二个线程去执行。假设目前我们的count的值是9,当第一个线程执行完count++操作时,正好被第二个线程打断,而此时第一个线程执行完count++操作后并没有把值写入内存,所以此时第二个线程在读取count的值时,count的值依旧是9。按正常逻辑来讲,两次count++操作后,count的值为11,然而在这样的一种情况下,两次count++操作后,count的值只是10。对于这样的一种现象,我们称作线程不安全

synchronized的两个用法:

对象锁

包括方法锁(默认锁对象为this当前实例对象)和同步代码块锁(自己指定锁对象)

代码块形式

手动指定锁对象

package synchronize;

/**
 * 描述 对象锁实例1,代码块形式
 */
public class SynchronizedObjectCodeBlock2 implements Runnable {

    static SynchronizedObjectCodeBlock2 instance = new SynchronizedObjectCodeBlock2();

    // 当两个线程一起调用该方法时,线程会进入阻塞状态,从而达到串行的效果,以保证线程安全
    @Override
    public void run() {
        // this值为SynchronizedObjectCodeBlock2对象
        synchronized (this) {
            System.out.println("this的值为:"+ this);
            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) {

        System.out.println("instance实例对象:" + instance);

        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();

        while (t1.isAlive() || t2.isAlive()) {

        }
        System.out.println("finished");


    }
}

方法锁形式

synchronized修饰普通方法, 锁对象默认为this

package synchronize;

/**
 * 描述:对象锁实例2,方法锁形式
 */
public class SynchronizedObjectMethods3 implements Runnable {

    static SynchronizedObjectMethods3 instance = new SynchronizedObjectMethods3();

    public static void main(String[] args) {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);

        t1.start();
        t2.start();

        while (t1.isAlive() || t2.isAlive()) {

        }
        System.out.println("finished");

    }

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

    // 当两个线程一起调用该方法时,线程会进入阻塞状态,从而达到串行的效果,以保证线程安全
    public synchronized void methods(){
        System.out.println("我是对象锁的方法修饰符形式,我叫"+ Thread.currentThread().getName());

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"运行结束");
    }

}


类锁

synchronized修饰静态的方法或指定锁Class对象。在java类中可能有很个对象,但只有1Class对象。所谓的类锁,其本质不过就是Class 对象锁而已。

类锁与对象锁的区别

类锁只能在同一时刻被一个对象拥有。如果有个对象进行竞争的话,线程与线程之间将会阻塞;但对象锁则不同,不同的对象锁之间是不会有任何干扰的,他们会并行执行,并不会造成阻塞。因为锁的当前对象是this同一个类中,当锁对象不一样的多个线程同时运行时,会同时执行;如果是类锁,且两个对象都属于同一个类,则意味着这两个对象共享同一把锁,这样一来则会进行阻塞

形式一:

static方法上加synchroized关键字。

package synchronize;

/**
 * 描述:类锁的第一种形式:静态方法锁
 */
public class SynchronizedClassStatic4 implements Runnable {

    static SynchronizedClassStatic4 instance1 = new SynchronizedClassStatic4();
    static SynchronizedClassStatic4 instance2 = new SynchronizedClassStatic4();

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

    public static void main(String[] args) {
        Thread t1 = new Thread(instance1);
        Thread t2 = new Thread(instance2);
        t1.start();
        t2.start();

        while (t1.isAlive() || t2.isAlive()) {

        }
        System.out.println("finished");
    }

    // TODO why? 为什么methods方法加上 static 关键字就会造成阻塞?没加static关键字时,是一种的并行的状态?
    //  原因是该方法加上static关键字后就会属于类锁(锁定的对象是class对象),
    //  而class对象又是唯一的,要争取资源必须等待,所以先后执行了;
    //  然而一旦去掉static关键字,methods方法则变成对象方法,因此不同的对象都执行各的run方法,
    //  即锁(this)对象不同,所以则是并行执行。
    //
    public static synchronized void methods(){
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("我是类锁的第一形式:static形式,我叫"+ Thread.currentThread().getName());

    }
}

形式二:

synchroized(*.class)代码块。

package synchronize;

/**
 * 描述:类锁的第二种形式:`synchroized`(*.class)代码块。
 */
public class SynchronizedClassClass5 implements Runnable {

    static SynchronizedClassClass5 instance1 = new SynchronizedClassClass5();
    static SynchronizedClassClass5 instance2 = new SynchronizedClassClass5();

    @Override
    public void run() {
        try {
            methods();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(instance1);
        Thread t2 = new Thread(instance2);
        t1.start();
        t2.start();

        while (t1.isAlive() || t2.isAlive()) {

        }
        System.out.println("finished");
    }

    // TODO 当使用 SynchronizedClassClass5.class 时,线程以串行的方式执行;
    //  当我们使用this关键字进行替换时,线程差不多是以并行的方式来执行 ? 为什么呢 ?
    private void methods() throws InterruptedException {
        synchronized (SynchronizedClassClass5.class){
            System.out.println("我是类锁的第二种形式:synchronized(*.class)。" +
                    "我叫"+ Thread.currentThread().getName());
            Thread.sleep(3000);
            System.out.println(Thread.currentThread().getName() + "运行结束");
        }
    }
}

思考?

  1. 两个线程同时访问一个对象的同步方法。

    • 会进行阻塞。原因是他们的锁对象是同一个,两者之间会形成互斥,以串行的方式执行。
  2. 两个线程访问的是两个对象的同步方法。

    • 这个时候,synchroized是不起作用的,它俩是并行执行,原因是他们的锁对象并不是同一个,两者之间并不会形成互斥。
  3. 两个线程访问的是synchroized的静态方法。

    • 锁生效,线程会进行阻塞,两者之间并会形成互斥,因为一旦在方法名上加上static关键字,则会构成类锁,多个对象共用一个方法。因此会造成阻塞。具体如SynchronizedClassStatic4案例所示。
  4. 同时访问同步方法与非同步方法(通过代码演示)。

package synchronize;

/**
 *  描述: 同时访问同步方法与非同步方法,执行结果:同时开始,同时结束,可见非同步方法不受到影响
 */
public class SynchronizedYesAndNo6 implements Runnable {

    static SynchronizedYesAndNo6 instance = new SynchronizedYesAndNo6();

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

    // 同步方法
    public synchronized void methods1(){
        System.out.println("我是加锁方法。我叫" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行结束");

    }

    // 非同步方法
    public void methods2(){
        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) {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        while (t1.isAlive() || t2.isAlive()) {

        }
        System.out.println("finished");
    }
}

  1. 访问同一个对象的不同的普通同步方法(代码如下)。
package synchronize;

/**
 * 描述: 同时访问一个类的不同的普通同步方法
 */
public class SynchronizedDifferenceMethods7 implements Runnable {

    static SynchronizedDifferenceMethods7 instance1 = new SynchronizedDifferenceMethods7();

    // TODO 从执行结果来看,虽然没有指定锁对象,但本质上是指定了this作为锁的对象 
    @Override
    public void run() {
        if(Thread.currentThread().getName().equals("Thread-0")){
            methods1();
        }else {
            methods2();
        }
    }

    // 同步方法
    public synchronized void methods1(){
        System.out.println("我是加锁方法。我叫" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行结束");

    }

    // 同步方法
    public synchronized void methods2(){
        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) {

        Thread t1 = new Thread(instance1);
        Thread t2 = new Thread(instance1);
        t1.start();
        t2.start();
        while (t1.isAlive() || t2.isAlive()) {

        }
        System.out.println("finished");

    }
}

  1. 同时访问静态synchronized和非静态synchronized的方法
package synchronize;

/**
 *  描述:同时访问静态synchronized和非静态synchronized的方法
 */
public class SynchronizedStaticAndNormal8 implements Runnable {

    static SynchronizedStaticAndNormal8 instance = new SynchronizedStaticAndNormal8();

    // TODO 从执行结果来看,两个线程同时开始,同时结束。这是因为两个线程走的是不同的方法,即线程锁对象并非是同一个。
    @Override
    public void run() {
        if(Thread.currentThread().getName().equals("Thread-0")){
            // 第一个线程走methods1()方法
            methods1();
        }else {
            // 第二个线程走methods2()方法
            methods2();
        }
    }

    // 非静态同步方法(该方法的锁对象为:对象实例本身)
    public synchronized void methods1(){
        System.out.println("我是非静态加锁方法。我叫" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行结束");

    }

    // 静态同步方法(该方法的锁对象为:类锁)
    public static synchronized void methods2(){
        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) {

        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        while (t1.isAlive() || t2.isAlive()) {

        }
        System.out.println("finished");

    }
}

  1. 方法抛出异常后,会释放锁吗?
    • lock不一样,synchronized在抛出异常后,会释放锁,而lock则需要在异常的`finaly中进行释放。
package synchronize;

/**
 * 描述: 方法抛弃后,会释放锁。
 * 展示不抛出异常前和抛出异常后的对比:一旦抛出异常,第二个线程会立刻进入同步方法,意味着锁已经释放
 */
public class SynchronizedException9 implements Runnable {

    static SynchronizedException9 instance = new SynchronizedException9();

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

    // 非静态同步方法
    public synchronized void methods1(){
        System.out.println("我是加锁方法。我叫" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        try {
            throw new Exception();
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行结束");

    }

    public synchronized void methods2(){
        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) {

        // TODO 这两个线程使用的是同一个对象,这意味着是同一个对象锁
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        while (t1.isAlive() || t2.isAlive()) {

        }
        System.out.println("finished");

    }
}

总结:

  1. 一把锁只能同时被一个线程获取,没有拿到锁的线程必须等待(如案例1和案例5)。
  2. 每个实例都对应有自己的一把锁,不同实例之间互不影响,例外:锁对象是.class以及synchronized修饰的是static方法的时候,所有对象共同使用一把类锁(如2,3,4,6案例所示)。
  3. synchronized代码块中,无论是正常使用还是在执行过程中抛出异常,锁都会得到释放(如案例7所示)。

在这里我们在补充一下,当线程调用synchronized关键字修饰的方法时,而这个方法中再次调用一个没有被synchronized关键字修饰的其他方法时,这个时候还是线程安全吗?

答案是否定的。一旦离开synchronized关键字修饰的方法,进入到另一个方法时,这个方法就有可以被多个线程同时访问。

参考

相关文章

网友评论

      本文标题:synchronized关键字(一)

      本文链接:https://www.haomeiwen.com/subject/asuvjhtx.html