synchronized关键字

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

在日常开发工作,很多情况会用到多线程,那么在多线程运行的环境下,就很难避免会涉及到一些共享数据。若出现多个线程同时访问操作同一共享数据的情况,可能会造成数据混乱的现象,破坏数据一致性。这个时候就要考虑使用“锁”来解决这一现象。“锁”见名知意,将某物锁起来,不让其他人用。那么在我们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();

    }
}

控制台数据结果

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

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

 

按照输出结果来分析,是一号柜台先抢占到了资源,然后二号柜台抢占到资源,最后三号柜台抢占到资源。当一号柜台抢占到资源进入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++) + "号顾客业务");
            }
        }
    }
}

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

由测试结果可见,一号柜台最先抢占到资源,按顺序完成了业务办理,但是还是存在问题,那就是第二柜台和第三柜台根本就没干活呀,这怎么能行呢?之所以出现这个问题的原因是我们在测试类中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;
        }
    }
}

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

由运行结果可知,一号、二号、三号柜台交替办理完了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是可重入锁。

假如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();
    }
}

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

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

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

输出结果:

 

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

 

Ending~~~~~~~~~

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值