欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

synchronized关键字

程序员文章站 2024-03-21 17:32:22
...

说在前面:文章内容为自己学在习过程中对知识的理解,如有不正确的之处,欢迎大家指正~~共同进步!!!

在日常开发工作,很多情况会用到多线程,那么在多线程运行的环境下,就很难避免会涉及到一些共享数据。若出现多个线程同时访问操作同一共享数据的情况,可能会造成数据混乱的现象,破坏数据一致性。这个时候就要考虑使用“锁”来解决这一现象。“锁”见名知意,将某物锁起来,不让其他人用。那么在我们Java程序开发中,锁的物就是我们的类、对象、代码块。本文主要说的是通过Java中synchronized关键字实现锁的效果。


首先整理synchronized的几个知识点:

1.synchronized修饰普通方法,锁的是该类的当前实例对象

2.synchronized修饰静态方法,锁的是该类(即该类的所有实例对象)

3.synchronized修饰代码块,锁的是synchronized(X)代码块中定义的X对象实例

4.synchronized代码块里代码单线程执行

5.synchronized具有可重入性

6.处于阻塞状态的线程即使调用了中断方法也不会生效

7.使用synchronized会降低效率


下面跟着代码进一步理解吧

情景描述:模拟银行柜台叫号的工作流程。多个柜台办理业务,多个顾客拿着各自的排号等待去柜台办理业务。

剖析:情景描述中的多个柜台就是多个需要执行任务的线程,每个顾客手中的排号就是“共享数据”


情景功能实现测试一:不加锁


package com.thread.demo;

/**
 * @Author: Peacock__
 * @Date: 2019/4/25 14:36
 */
public class BankThread implements Runnable {

    //当前排号:当前办理的排号(在情景中充当共享数据的身份)
    private Integer CURRENT_NUMBER = 1;

    //最大排号:最多办理3个顾客的业务就要去开集体临时会议了
    private final static Integer MAX_NUMBER = 3;

    @Override
    public void run() {
        while(true){
            //当前排号大于最大排号时,任务执行完毕
            if(CURRENT_NUMBER > MAX_NUMBER){
                break;
            }

            //休眠5毫秒,模拟正在办理业务
            try {
                Thread.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            //控制台输出,并将当前排号+1,模拟叫下一位顾客
            System.out.println(Thread.currentThread().getName() + "办理完毕"+(CURRENT_NUMBER++)+"号顾客业务");
        }
    }
}
package com.thread.demo;

/**
 * @Author: Peacock__
 * @Date: 2019/4/25 14:39
 */
public class BankTest {

    public static void main(String[] args) {
        final BankThread bankThread = new BankThread();

        Thread threadA = new Thread(bankThread,"一号柜台");
        Thread threadB = new Thread(bankThread,"二号柜台");
        Thread threadC = new Thread(bankThread,"三号柜台");

        threadA.start();
        threadB.start();
        threadC.start();

    }
}

控制台数据结果

synchronized关键字

看到运行结果我们可以看到“一号柜台”、“二号柜台”、“三号柜台”谁抢占到了资源谁就办理了业务,没有先后顺序之分。但是肯定会很诧异,为什么我们明明规定了最多办理3个客户,为什么一号柜台还给4号顾客办理了业务????解释如下图

注意:不要被控制台输出语句的顺序影响,要按照CURRENT_NUMBER的值来分析。

synchronized关键字

 

按照输出结果来分析,是一号柜台先抢占到了资源,然后二号柜台抢占到资源,最后三号柜台抢占到资源。当一号柜台抢占到资源进入run方法后,直接进入while循环,当前的CURRENT_NUMBER是1,不符合break条件, 因为没有加锁,所以在第一柜台执行run方法的时候,第二柜台已经抢占到了资源(注意这个时候如果第一柜台第一次循环执行完毕了,那么CURRENT_NUMBER = 2,如果第一柜台第一次循环没有执行完毕了,那么CURRENT_NUMBER = 1) 按照输出结果可知此时第一柜台没有执行完毕,所以此时CURRENT_NUMBER = 1不符合break条件,所以第二柜台也会进入sleep状态, 正当第二柜台在sleep的时候第三柜台进来了,按照输出结果可知此时第一柜台、第二柜台都没执行完毕,所以此时依旧CURRENT_NUMBER = 1,也不符合break条件,所以第三柜台也会进入sleep状态, 按照输出结果依旧可知,第三柜台进来后,第一柜台第一次执行完毕,进行第二次执行,此时第二柜台和第三柜台都没有执行完毕,所以此时CURRENT_NUMBER = 2不符合break条件,所以第一柜台第二次执行进入sleep状态, 就在这时可能第二柜台执行完毕了,CURRENT_NUMBER变成了2,紧接着第三柜台执行完毕CURRENT_NUMBER变成了3,这时不管哪个柜台再进入循环都符合break程序结束条件。但是就在马上要执行break的时候, 第一柜台的第二次执行也执行完毕了,输出了“一号柜台办理完毕4号顾客业务”这句话,所以CURRENT_NUMBER = 4。刚好输出完这句话,break执行了,程序彻底结束。

 


 情景功能实现测试二:加锁,锁定类的当前实例


package com.thread.demo;

/**
 * @Author: Peacock__
 * @Date: 2019/4/25 14:36
 */
public class BankThread implements Runnable {

    //当前排号:当前办理的排号
    private Integer CURRENT_NUMBER = 1;

    //最大排号:为了更好的展现效果,调整最多办理30个顾客的业务再去开集体临时会议了
    private final static Integer MAX_NUMBER = 30;

    @Override
    public void run() {
        //this为BankThread类的当前实例
        synchronized (this) {
            while (true) {
                //当前排号大于最大排号时,任务执行完毕
                if (CURRENT_NUMBER > MAX_NUMBER) {
                    break;
                }

                //休眠5毫秒,模拟正在办理业务
                try {
                    Thread.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                //控制台输出,并将当前排号+1,模拟叫下一位顾客
                System.out.println(Thread.currentThread().getName() + "办理完毕" + (CURRENT_NUMBER++) + "号顾客业务");
            }
        }
    }
}

测试类还是用上面测试的测试类,没有任何改动,这时我们再运行测试,运行结果如下:

synchronized关键字

由测试结果可见,一号柜台最先抢占到资源,按顺序完成了业务办理,但是还是存在问题,那就是第二柜台和第三柜台根本就没干活呀,这怎么能行呢?之所以出现这个问题的原因是我们在测试类中new Thread的时候,都是通过同一个BankThread的实例来创建的,所以第一柜台、第二柜台、第三柜台线程都是通过同一BankThread的实例来访问run方法。刚好我们加run方法里的锁是锁定BankThread的当前实例的,所以当第一柜台进来后就持有了BankThread的当前实例的锁,第二柜台、第三柜台进来run方法后就需要一直等待获取BankThread的当前实例的锁。当第一柜台执行完毕后释放了BankThread的当前实例的锁,但是这个时候不管哪个柜台在持有BankThread的当前实例的锁,进入while后,都满足break条件退出循环,程序结束。


 情景功能实现测试三:加锁,锁定代码块


package com.thread.demo;

/**
 * @Author: Peacock__
 * @Date: 2019/4/25 14:36
 */
public class BankThread implements Runnable {

    //当前排号:当前办理的排号
    private Integer CURRENT_NUMBER = 1;

    //最大排号:为了更好的展现效果,调整最多办理500个顾客的业务再去开集体临时会议了
    private final static Integer MAX_NUMBER = 500;

    //定义私有唯一对象
    private final static Object LOCK = new Object();

    @Override
    public void run() {
        //此部分不加锁
        while (true) {
            //do_job()中加锁
            if (do_job()) {
                break;
            }
        }
    }

    private boolean do_job() {
        //锁定LOCK对象,而非当前类的实例
        synchronized (LOCK) {
            //当前排号大于最大排号时,任务执行完毕
            if (CURRENT_NUMBER > MAX_NUMBER) {
                return true;
            }

            //休眠5毫秒,模拟正在办理业务
            try {
                Thread.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            //控制台输出,并将当前排号+1,模拟叫下一位顾客
            System.out.println(Thread.currentThread().getName() + "办理完毕" + (CURRENT_NUMBER++) + "号顾客业务");

            return false;
        }
    }
}

测试类还是用上面测试的测试类,没有任何改动,这时我们再运行测试,运行结果如下:

synchronized关键字

由运行结果可知,一号、二号、三号柜台交替办理完了500个顾客的业务,正是我们想要的效果。原因是我们锁定的是LOCK对象而并非BankThread类的当前实例,一号、二号、三号柜台都能够进入run方法,并进入while循环,在while循环中进入do_job方法,进入do_job后会持有LOCK的锁,执行完一次do_job方法后,就会释放LOCK的锁,由其他线程抢占接替持有LOCK的锁。直至do_job返回false,满足break条件退出循环,程序结束。


 最后我们模拟一下synchronized修饰静态方法,验证一下锁定的是否为当前类的所有实例。


package com.thread.demo;

/**
 * @Author: Peacock__
 * @Date: 2019/4/25 17:45
 */
public class StaticDemo {

    public synchronized static void s1() {
        try {
            Thread.sleep(10_000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"---s1执行完毕");
    }

    public synchronized static void s2() {
        try {
            Thread.sleep(10_000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"---s2执行完毕");
    }

    public static void s3() {
        System.out.println(Thread.currentThread().getName()+"---s3执行完毕");
    }
}
package com.thread.demo;

/**
 * @Author: Peacock__
 * @Date: 2019/4/25 14:39
 */
public class BankTest {

    public static void main(String[] args) {
        new Thread("t1"){
            @Override
            public void run() {
                StaticDemo.s1();
            }
        }.start();

        new Thread("t2"){
            @Override
            public void run() {
                StaticDemo.s2();
            }
        }.start();


        new Thread("t3"){
            @Override
            public void run() {
                StaticDemo.s3();
            }
        }.start();

    }
}

跑测试类输出如图:

synchronized关键字

根据输出结果的描述,验证了synchronized修饰静态方法锁定的是当前类的所有实例 


synchronized具有可重入性


当线程请求一个由其它线程持有的对象锁时,该线程会阻塞,而当线程请求由自己持有的对象锁时,如果该锁是重入锁,请求就会成功,否则阻塞。synchronized是可重入锁。

假如objectA中有两个synchronized修饰的方法a、b,且在a方法中调用了b方法

这里的重入性是指当前线程获取到某对象(objectA)锁后,先执行a方法,在a中调用b方法的时候,由于b方法也被synchronized修饰。所以还需要获取objectA的对象锁,由于此时objectA的对象锁的持有者是自己,所以当前线程可以无需等待获取锁,而是可以直接正常执行b方法。需要特别注意另外一种情况,当子类继承父类时,子类也是可以通过可重入锁调用父类的同步方法

package com.thread.demo;

/**
 * @Author: Peakcock__
 * @Date: 2019/4/28 14:24
 */
public class SynchronizedDemo implements Runnable{

    private int x;

    private int y;

    public synchronized void a() {
        for(int i = 0 ; i < 5 ; i ++){
            x++;
            b();
        }
    }

    public synchronized void b() {
        y++;
    }

    public static void main(String[] args) {
        SynchronizedDemo demo = new SynchronizedDemo();

        Thread t = new Thread(demo);
        t.start();
        
    }

    @Override
    public void run() {
        a();
        System.out.println(x +"----"+y);//输出结果:5----5
    }
}

处于阻塞状态的线程即使调用了中断方法也不会生效(Thread.interrupt()无法中断非阻塞状态下的线程


对于synchronized来说,如果一个线程正在等待锁,结果只有两种,要么获得锁,要么继续等待,处于阻塞状态的线程即使调用了中断方法也不会被中断,但是会抛出InterrupterException异常,同时会立即复位(中断状态改为非中断状态)。

package com.thread.demo;

import java.util.concurrent.TimeUnit;

/**
 * @Author: Peakcock__
 * @Date: 2019/4/28 14:24
 */
public class SynchronizedDemo implements Runnable{

    @Override
    public void run() {
        try{
            while(true){
                //休眠2秒
                TimeUnit.SECONDS.sleep(2);
            }
        }catch(Exception e){
            //捕获异常,查看中断状态
            System.out.println("当前线程是否被打断:"+Thread.currentThread().isInterrupted());
        }
    }

    public static void main(String[] args) {
        Thread t = new Thread(new SynchronizedDemo());
        t.start();
        //中断线程
        t.interrupt();
    }
}

输出结果,验证了我们上面的描述

synchronized关键字

好,那既然我们已经验证了“处于阻塞状态的线程无法被中断”,那是不是意味着非阻塞的线程就可以被中断呢?我们来试一下:

package com.thread.demo;

import java.util.concurrent.TimeUnit;

/**
 * @Author: Peakcock__
 * @Date: 2019/4/28 14:24
 */
public class SynchronizedDemo implements Runnable{

    @Override
    public void run() {
        try{
            while(true){
                System.out.println("线程未被打断");
            }
        }catch(Exception e){
            //捕获异常,查看中断状态
            System.out.println("当前线程是否被打断:"+Thread.currentThread().isInterrupted());
        }
    }

    public static void main(String[] args) {
        Thread t = new Thread(new SynchronizedDemo());
        t.start();
        //中断线程
        t.interrupt();
    }
}

但是发现输出结果是无限输出“线程未被打断”,额....................那到底怎样才能中断线程呢??????

原来是处于非阻塞状态的线程,我们需要手动进行中断检测后才能被中断,如下:

package com.thread.demo;

import java.util.concurrent.TimeUnit;

/**
 * @Author: Peakcock__
 * @Date: 2019/4/28 14:24
 */
public class SynchronizedDemo implements Runnable{

    @Override
    public void run() {
        try{
            while(true){
                //中断检测
                if(Thread.currentThread().isInterrupted()){
                    System.out.println("线程被打断");
                    break;
                }
                System.out.println("线程未被打断");
            }
        }catch(Exception e){
            //捕获异常,查看中断状态
            System.out.println("当前线程是否被打断:"+Thread.currentThread().isInterrupted());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new SynchronizedDemo());
        t.start();
        //休眠1秒
        TimeUnit.MILLISECONDS.sleep(1);
        //中断线程
        t.interrupt();
    }
}

输出结果:

 synchronized关键字

OK,输出结果已经验证“处于非阻塞状态的线程,我们需要手动进行中断检测后才能被中断”。

 

Ending~~~~~~~~~

哈哈^_^今天又是收获满满的一天(^-^)V

相关标签: synchronized