Java多线程6:synchronized锁定类方法、volatile关键字及其他

摘要:
同步静态方法synchronized还可以应用在静态方法上,如果这么写,则代表的是对当前.java文件对应的Class类加锁。volatile关键字直接先举一个例子:publicclassMyThread28extendsThread{privatebooleanisRunning=true;publicbooleanisRunning(){returnisRunning;}publicvoidsetRunning{this.isRunning=isRunning;}publicvoidrun(){System.out.println;while{}System.out.println;}}publicstaticvoidmain{try{MyThread28mt=newMyThread28();mt.start();Thread.sleep;mt.setRunning;System.out.println;}catch{e.printStackTrace();}}看一下运行结果:进入run了已赋值为false也许这个结果有点奇怪,明明isRunning已经设置为false了,线程还没停止呢?解决这个问题很简单,给isRunning关键字加上volatile。加上了volatile的意思是,每次读取isRunning的值的时候,都先从主内存中把isRunning同步到线程的工作内存中,再当前时刻最新的isRunning。

同步静态方法

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

public classThreadDomain25
{
    public synchronized static voidprintA()
    {
        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 voidprintB()
    {
        System.out.println("线程名称为:" + Thread.currentThread().getName() + 
                "在" + System.currentTimeMillis() + "进入printB()方法");
        System.out.println("线程名称为:" + Thread.currentThread().getName() + 
                "在" + System.currentTimeMillis() + "离开printB()方法");
    }
    public synchronized voidprintC()
    {
        System.out.println("线程名称为:" + Thread.currentThread().getName() + 
                "在" + System.currentTimeMillis() + "进入printC()方法");
        System.out.println("线程名称为:" + Thread.currentThread().getName() + 
                "在" + System.currentTimeMillis() + "离开printC()方法");
    }
}

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

public class MyThread25_0 extendsThread
{
    public voidrun()
    {
        ThreadDomain25.printA();
    }
}
public class MyThread25_1 extendsThread
{
    public voidrun()
    {
        ThreadDomain25.printB();
    }
}
public class MyThread25_2 extendsThread
{
    privateThreadDomain25 td;
    publicMyThread25_2(ThreadDomain25 td)
    {
        this.td =td;
    }
    public voidrun()
    {
        td.printC();
    }
}

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

public static voidmain(String[] args)
{
    ThreadDomain25 td = newThreadDomain25();
    MyThread25_0 mt0 = newMyThread25_0();
    MyThread25_1 mt1 = newMyThread25_1();
    MyThread25_2 mt2 = newMyThread25_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 extendsThread
{
    private boolean isRunning = true;
    public booleanisRunning()
    {
        returnisRunning;
    }
    public void setRunning(booleanisRunning)
    {
        this.isRunning =isRunning;
    }
    public voidrun()
    {
        System.out.println("进入run了");
        while (isRunning == true){}
        System.out.println("线程被停止了");
    }
}
public static voidmain(String[] args)
{
    try
    {
        MyThread28 mt = newMyThread28();
        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 classThreadDomain29
{
    public static AtomicInteger aiRef = newAtomicInteger();
    public voidaddNum()
    {
        System.out.println(Thread.currentThread().getName() + "加了100之后的结果:" +
                aiRef.addAndGet(100));
        aiRef.getAndAdd(1);
    }
}
public class MyThread29 extendsThread
{
    privateThreadDomain29 td;
    publicMyThread29(ThreadDomain29 td)
    {
        this.td =td;
    }
    public voidrun()
    {
        td.addNum();
    }
}
public static voidmain(String[] args)
{
    try
    {
        ThreadDomain29 td = newThreadDomain29();
        MyThread29[] mt = new MyThread29[5];
        for (int i = 0; i < mt.length; i++)
        {
            mt[i] = newMyThread29(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之后的结果:200
Thread-4加了100之后的结果:500
Thread-3加了100之后的结果:400
Thread-2加了100之后的结果:300
Thread-0加了100之后的结果:100
505

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

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

免责声明:文章转载自《Java多线程6:synchronized锁定类方法、volatile关键字及其他》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇【maven】之打包war依赖子项目jarbox2d HelloWorld下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

重新理解:ASP.NET 异步编程

相关博文: 异步编程 In .NET(回味无穷!!!) ASP.NET sync over async(异步中同步,什么鬼?) 本来这篇博文想探讨下异步中的异常操作,但自己在做异步测试的时候,又对 ASP.NET 异步有了新的认识,可以说自己之前对异步的理解还是有些问题,先列一下这篇博文的三个解惑点: async await 到底是什么鬼??? 异...

Python中的多线程编程,线程安全与锁(一)

1.多线程编程与线程安全相关重要概念 在我的上篇博文聊聊Python中的GIL中,我们熟悉了几个特别重要的概念:GIL,线程,进程,线程安全,原子操作。 以下是简单回顾,详细介绍请直接看聊聊Python中的GIL GIL:Global Interpreter Lock,全局解释器锁。为了解决多线程之间数据完整性和状态同步的问题,设计为在任意时刻只有一个...

当析构函数遇到多线程 ── C++ 中线程安全的对象回调

陈硕 (giantchen_AT_gmail) 本文 PDF  下载: http://www.cppblog.com/Files/Solstice/dtor_meets_mt.pdf 摘要 编写线程安全的类不是难事,用同步原语保护内部状态即可。但是对象的生与死不能由对象自身拥有的互斥器来保护。如何保证即将析构对象 x 的时候,不会有另一个线程正在调用...

卡顿问题

导致卡顿问题的几种原因: 复杂 UI 、图文混排的绘制量过大; 在主线程上做网络同步请求; 在主线程做大量的 IO 操作; 运算量过大,CPU 持续高占用; 死锁和主子线程抢锁。 FPS 是一秒显示的帧数,也就是一秒内画面变化数量。当FPS达到60,说明界面很流程,当FPS低于24,页面流畅度不是那么流畅。 线程的消息 事件是依赖于 NSRunLoop...

WPF 同一窗口内的多线程 UI(VisualTarget)

WPF 的 UI 逻辑只在同一个线程中,这是学习 WPF 开发中大家几乎都会学习到的经验。如果希望做不同线程的 UI,大家也会想到使用另一个窗口来实现,让每个窗口拥有自己的 UI 线程。然而,就不能让同一个窗口内部使用多个 UI 线程吗? 答案其实是——可以的!使用 VisualTarget 即可。 阅读本文将收获一份对 VisualTarget 的解读以...

c++中CreateEvent函数

参考:https://blog.csdn.net/u011642774/article/details/52789969 函数原型: [cpp] view plain copy  HANDLE CreateEvent(     LPSECURITY_ATTRIBUTES lpEventAttributes, // SD     BOOL bMan...