博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Java多线程6:synchronized锁定类方法、volatile关键字及其他
阅读量:6967 次
发布时间:2019-06-27

本文共 5479 字,大约阅读时间需要 18 分钟。

同步静态方法

synchronized还可以应用在静态方法上,如果这么写,则代表的是对当前.java文件对应的Class类加锁。看一下例子,注意一下printC()并不是一个静态方法:

public class ThreadDomain25{    public synchronized static void printA()    {        try        {            System.out.println("线程名称为:" + Thread.currentThread().getName() +                     "在" + System.currentTimeMillis() + "进入printA()方法");            Thread.sleep(3000);            System.out.println("线程名称为:" + Thread.currentThread().getName() +                     "在" + System.currentTimeMillis() + "离开printA()方法");        }        catch (InterruptedException e)        {            e.printStackTrace();        }    }        public synchronized static void printB()    {        System.out.println("线程名称为:" + Thread.currentThread().getName() +                 "在" + System.currentTimeMillis() + "进入printB()方法");        System.out.println("线程名称为:" + Thread.currentThread().getName() +                 "在" + System.currentTimeMillis() + "离开printB()方法");    }        public synchronized void printC()    {        System.out.println("线程名称为:" + Thread.currentThread().getName() +                 "在" + System.currentTimeMillis() + "进入printC()方法");        System.out.println("线程名称为:" + Thread.currentThread().getName() +                 "在" + System.currentTimeMillis() + "离开printC()方法");    }}

写三个线程分别调用这三个方法:

public class MyThread25_0 extends Thread{    public void run()    {        ThreadDomain25.printA();    }}
public class MyThread25_1 extends Thread{    public void run()    {        ThreadDomain25.printB();    }}
public class MyThread25_2 extends Thread{    private ThreadDomain25 td;        public MyThread25_2(ThreadDomain25 td)    {        this.td = td;    }        public void run()    {        td.printC();    }}

写个main函数启动这三个线程:

public static void main(String[] args){    ThreadDomain25 td = new ThreadDomain25();    MyThread25_0 mt0 = new MyThread25_0();    MyThread25_1 mt1 = new MyThread25_1();    MyThread25_2 mt2 = new MyThread25_2(td);    mt0.start();    mt1.start();    mt2.start();}

看一下运行结果:

线程名称为:Thread-0在1443857019710进入printA()方法线程名称为:Thread-2在1443857019710进入printC()方法线程名称为:Thread-2在1443857019710离开printC()方法线程名称为:Thread-0在1443857022710离开printA()方法线程名称为:Thread-1在1443857022710进入printB()方法线程名称为:Thread-1在1443857022710离开printB()方法

从运行结果来,对printC()方法的调用和对printA()方法、printB()方法的调用时异步的,这说明了静态同步方法和非静态同步方法持有的是不同的锁,前者是类锁,后者是对象锁

所谓类锁,举个再具体的例子。假如一个类中有一个静态同步方法A,new出了两个类的实例B和实例C,线程D持有实例B,线程E持有实例C,只要线程D调用了A方法,那么线程E调用A方法必须等待线程D执行完A方法,尽管两个线程持有的是不同的对象。

 

volatile关键字

直接先举一个例子:

public class MyThread28 extends Thread{    private boolean isRunning = true;    public boolean isRunning()    {        return isRunning;    }    public void setRunning(boolean isRunning)    {        this.isRunning = isRunning;    }        public void run()    {        System.out.println("进入run了");        while (isRunning == true){}        System.out.println("线程被停止了");    }}
public static void main(String[] args){    try    {        MyThread28 mt = new MyThread28();        mt.start();        Thread.sleep(1000);        mt.setRunning(false);        System.out.println("已赋值为false");    }    catch (InterruptedException e)    {        e.printStackTrace();    }}

看一下运行结果:

进入run了已赋值为false

也许这个结果有点奇怪,明明isRunning已经设置为false了, 线程还没停止呢?

这就要从Java内存模型(JMM)说起,这里先简单讲,虚拟机那块会详细讲的。根据JMM,Java中有一块主内存,不同的线程有自己的工作内存,同一个变量值在主内存中有一份,如果线程用到了这个变量的话,自己的工作内存中有一份一模一样的拷贝。每次进入线程从主内存中拿到变量值,每次执行完线程将变量从工作内存同步回主内存中。

出现打印结果现象的原因就是主内存和工作内存中数据的不同步造成的。因为执行run()方法的时候拿到一个主内存isRunning的拷贝,而设置isRunning是在main函数中做的,换句话说 ,设置的isRunning设置的是主内存中的isRunning,更新了主内存的isRunning,线程工作内存中的isRunning没有更新,当然一直死循环了,因为对于线程来说,它的isRunning依然是true。

解决这个问题很简单,给isRunning关键字加上volatile。加上了volatile的意思是,每次读取isRunning的值的时候,都先从主内存中把isRunning同步到线程的工作内存中,再当前时刻最新的isRunning。看一下给isRunning加了volatile关键字的运行效果:

进入run了已赋值为false线程被停止了

看到这下线程停止了,因为从主内存中读取了最新的isRunning值,线程工作内存中的isRunning变成了false,自然while循环就结束了。

volatile的作用就是这样,被volatile修饰的变量,保证了每次读取到的都是最新的那个值。线程安全围绕的是可见性原子性这两个特性展开的,volatile解决的是变量在多个线程之间的可见性,但是无法保证原子性

多提一句,synchronized除了保障了原子性外,其实也保障了可见性。因为synchronized无论是同步的方法还是同步的代码块,都会先把主内存的数据拷贝到工作内存中,同步代码块结束,会把工作内存中的数据更新到主内存中,这样主内存中的数据一定是最新的。

 

原子类也无法保证线程安全

原子操作表示一段操作是不可分割的,没有其他线程能够中断或检查正在原子操作中的变量。一个原子类就是一个原子操作可用的类,它可以在没有锁的情况下保证线程安全。

但是这种线程安全不是绝对的,在有逻辑的情况下输出结果也具有随机性,比如

public class ThreadDomain29{    public static AtomicInteger aiRef = new AtomicInteger();        public void addNum()    {        System.out.println(Thread.currentThread().getName() + "加了100之后的结果:" +                 aiRef.addAndGet(100));        aiRef.getAndAdd(1);    }}
public class MyThread29 extends Thread{    private ThreadDomain29 td;        public MyThread29(ThreadDomain29 td)    {        this.td = td;    }        public void run()    {        td.addNum();    }}
public static void main(String[] args){    try    {        ThreadDomain29 td = new ThreadDomain29();        MyThread29[] mt = new MyThread29[5];        for (int i = 0; i < mt.length; i++)        {            mt[i] = new MyThread29(td);        }        for (int i = 0; i < mt.length; i++)        {            mt[i].start();        }        Thread.sleep(1000);        System.out.println(ThreadDomain29.aiRef.get());    }     catch (InterruptedException e)    {        e.printStackTrace();    }}

这里用了一个Integer的原子类AtomicInteger,看一下运行结果:

Thread-1加了100之后的结果:200Thread-4加了100之后的结果:500Thread-3加了100之后的结果:400Thread-2加了100之后的结果:300Thread-0加了100之后的结果:100505

显然,结果是正确的,但不是我们想要的,因为我们肯定希望按顺序输出加了之后的结果,现在却是200、500、400、300、100这么输出。导致这个问题产生的原因是aiRef.addAndGet(100)和aiRef.addAndGet(1)这两个操作是可分割导致的。

解决方案,就是给addNum方法加上synchronized即可。

转载地址:http://qnisl.baihongyu.com/

你可能感兴趣的文章
阿里云RPA(机器人流程自动化)干货系列之六:客户端安装及激活 ...
查看>>
Alibaba Cluster Data 开放下载:270 GB 数据揭秘你不知道的阿里巴巴数据中心 ...
查看>>
Decision Tree
查看>>
四万高手过招,这份阿里全球数学竞赛试题你真的不要看吗 ...
查看>>
进行移动端Unity开发,针对引擎PowerVR性能建议
查看>>
linux gcc Makefile
查看>>
SpringBoot-Security-用户权限分配-配置验证规则
查看>>
学习大数据要掌握哪些语言?需要学习哪些内容? ...
查看>>
2019阿里云峰会-边缘计算专场,邀您共话大连接低时延场景下的技术探索与实践 ...
查看>>
云栖专辑| 阿里毕玄:程序员的成长路线
查看>>
jvm知识点总览
查看>>
如何在Windows 10 / 8.1 / 8中安装PHP或XAMP
查看>>
地铁译:Spark for python developers ---Spark的数据戏法
查看>>
举个栗子:专有云MaxCompute创建TableStore外部表
查看>>
网站服务器设置禁PING的方法步骤
查看>>
Linux基础命令---lpstat查看打印机状态
查看>>
新年福利 | 架构的“一小步”,业务的一大步
查看>>
Confluence 6 升级中的一些常见问题
查看>>
不可小看的数值类型—Python基础前传(5)
查看>>
集群中节点挂载数据盘的几种方式
查看>>