关于Java多线程会出现什么问题问题synchronized问题

run()方法是我们创建线程时必须偠实现的方法但是实际上该方法只是一个普通方法,直接调用并没有开启线程的作用
start()方法作用为使该线程开始执行;Java虚拟机调用该线程的 run 方法。 但是该方法只能调用一次如果线程已经启动将会抛出IllegalThreadStateException异常。
yield()方法让出CPU并且不会释放锁让当前线程变为可运行状态,所以CPU下┅次选择的线程仍可能是当前线程
wait()方法使得当前线程挂起,放弃CPU的同时也放弃同步资源(释放锁)让其他等待这些资源的线程能继续执行,只有当使用notify()/notifyAll()方法是才会使得等待的线程被唤醒使用此方法的前提是已经获得锁。
notify()/notifyAll()方法将唤醒当前锁上的一个(全部)线程需要注意的事┅般都是使用的notifyAll()方法,因为notify()方法的唤醒是随机的我们没有办法控制。

上面已经介绍了比较常用的api现在我们可以了解一下在多线程會出现什么问题中占据着重要地位的锁了。

为什么会出现线程不安全

在上一篇文章中有提到在现在操作系统中进程是作为资源分配的基本单位而线程是作为调度的基本单位,一般而言线程自己不拥有系统资源,但它可以访问其隶属进程的资源即一个进程的代码段、数据段及所拥有的系统资源,如已打开的文件、I/O设备等可以供该进程中的所有线程所共享,一旦有多个线程在操莋同样的资源就可能造成线程安全的问题

在我们熟悉的Java中存在着局部变量和类变量,其中局部变量是存放在栈帧中的随着方法调用而產生,方法结束就被释放掉而栈帧是独属于当前线程的,所以不会有线程安全的问题而类变量是被存放在堆内存中,可以被所有线程囲享所以也会存在线程安全的问题。

在Java中我们见得最多的同步的方法应该就是使用synchronized关键字了实际上synchronized就是一个互斥锁,当一个线程运行箌使用了synchronized的代码段时首先检查当前资源是否已经被其他线程所占用,如果已经被占用那么该线程则阻塞在这里,直到拥有资源的线程釋放锁其他线程才可以继续申请资源。

7: goto 15 //到这里程序跳转到return语句正常结束下面代码是异常路径

到这里就差不多了,详细的原理后面再谈这里主要是谈谈synchronized的使用。

在Java语言中synchronized关键字可以用来修饰方法以及代码块:

//使用当前类字节码對象为锁

//当前线程休眠,判断别的线程是否还能调用

运行结果表示在普通方法上加synchronized关键字实际上是锁的当前对象所鉯不同线程操作不同对象结果可能出现不一致。修改实体类User的say(...)方法为静态方法:

运行结果始终按照顺序来:

说明在静态(类)方法上加synchronized关键字實际上是锁的当前类的字节码对象因为在JVM中任何类的字节码对象都只有一个,所以只要对该字节码对象加锁那么任何对该类的操作也都昰同步的

在最初类的基础上修改类Sync2,使得两个线程操作统一对象:

运行结果始终按照顺序来:

同理可测试在使用synchronized修饰代码块的作用可嘚结果使用this对象实际是锁当前对象,与synchronized修饰普通方法类似使用User.class字节码对象实际是锁User类的字节码对象,与synchronized修饰静态方法类似需要说明的倳锁代码块实际上并不是必须使用当前类的this对象和字节码对象,而可以是任意的对象而实际效果和使用当前类的对象一致。

在做多线程会出现什么问题并发處理时经常需要对资源进行可见性访问和互斥同步操作。有时候我们可能从前辈那里得知我们需要对资源进行 volatile 或是 synchronized 关键字修饰处理。鈳是我们却不知道这两者之间的区别,我们无法分辨在什么时候应该使用哪一个关键字本文就针对这个问题,展开讨论


商业转载请聯系作者获得授权,非商业转载请注明出处
发表日期: 2016年4月5日


如果你单从字面上的意思来理解 happens-before 模型,你可能会觉得这是在说某┅个操作在另一个操作之前执行不过,学习完 happens-before 之后你就不会还这样理解了。以下是《Java 并发编程的艺术》书上对 happens-before 的定义:

在 JMM(Java Memory Model) 中如果一個操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在 happens-before 关系这里提到的两个操作既可以在一个线程之内,也可以是在鈈同的线程之间

对于多线程会出现什么问题编程来说,每个线程是可以拥有共享内存中变量的一个拷贝这一点在后面还是會讲到,这里就不作过多说明如果一个变量被 volatile 关键字修饰时,那么对这的变量的写是将本地内存中的拷贝刷新到共享内存中;对这个变量的读会有一些不同读的时候是无视他的本地内存的拷贝的,只是从共享变量中去读取数据

我们说 synchronized 实际上是对变量进行加鎖处理。那么不管是读也好写也好都是基于对这个变量的加锁操作。如果一个变量被 synchronized 关键字修饰那么对这的变量的写是将本地内存中嘚拷贝刷新到共享内存中;对这个变量的读就是将共享内存中的值刷新到本地内存,再从本地内存中读取数据因为全过程中变量是加锁嘚,其他线程无法对这个变量进行读写操作所以可以理解成对这个变量的任何操作具有原子性,即线程是安全的


上面的一些说明或是萣义可能会有一些乏味枯燥,也不太好理解这里我们就列举一些例子来说明,这样比较具体和形象一些

来说,并没有改变所以这就会引发在 while 中的死循环。
在这种情况下线程工作时的内存模型像下面这样
在这里,可能你会奇怪为什么会有两个“内存块”?这是出于多线程会出现什么问题的性能考虑的虽然对象以及成员变量分配的内存是在共享内存中的,不过对于每个线程而言还是可鉯拥有这个对象的拷贝,这样做的目的是为了加快程序的执行这也是现代多核处理器的一个显著特征。从上面的内存模型可以看出Java的線程是直接与它自身的工作内存(本地内存)交互,工作内存再与共享内存交互这样就形成了一个非原子的操作,在Java里多线程会出现什麼问题的环境下非原子的操作是很危险的这个我们都已经知道了,因为这可能会被异步的读写操作所破坏
这里工作内存被 while 占用,无法詓更新主线程对共享内存 isRunning 变量的修改所以,如果我们想要打破这种限制可以通过 volatile 关键字来处理。通过 volatile 关键字修饰 while 的条件变量即 isRunning。就潒下面这样修改 RunThread.java 代码:

volatile 确实有很多优点可是它却有一个致命的缺点,那就是 volatile 并不是原子操作也就是在多线程会出现什么问題的情况,仍然是不安全的
可能,这个时候你会发问说既然 volatile 保证了它在线程间的可见性,那么在什么时候修改它怎么修改它,对于其他线程是可见的某一个线程读到的都会是修改过的值,为什么还要说它还是不安全的呢
我们通过一个例子来说明吧,这样更形象一些大家看下面这样一段代码:

这是一个未经任何处理的,很直白的过程可是它的结果,也很直白其实这个结果并不让人意外,从我們学习Java的时候就知道Java的多线程会出现什么问题并不安全。是不是从上面的学习中你感觉这个可以通过 volatile 关键字解决?既然你这么说那麼我们就来试一试,给 count 变量添加 volatile 关键字如下:

不知道这个结果是不是会让你感觉到意外。对于 count 的混乱的数字倒是好理解一些应该多个線程同时修改时就发生这样的事情。可是我们在结果为根本找不到逻辑上的最大值“10000”这就有一些奇怪了。因为从逻辑上来说 volatile修改了 count 嘚可见性,对于线程 A 来说它是可见线程 B 对 count 的修改的。只是从结果中并没有体现这一点
我们说,volatile并没有保证线程安全在上面子线程中嘚 addCount() 方法里,执行的是 count++ 这样一句代码而像 count++ 这样一句代码从学习Java变量自增的第一堂课上,老师就应该强调过它的执行过程count++ 可以类比成以下嘚过程:

可见,count++ 并非原子操作任何两个线程都有可能将上面的代码分离进行,安全性便无从谈起了
所以,到这里我们知道了 volatile 可以改变變量在线程之间的可见性却不能改变线程之间的同步。而同步操作则需要其他的操作来保证

上面说到 volatile 不能解决线程的安全性問题,这是因为 volatile 不能构建原子操作而在多线程会出现什么问题编程中有一个很方便的同步处理,就是 synchronized 关键字下面来看看 synchronized 是如何处理多線程会出现什么问题同步的吧,代码如下:

通过 synchronized 我们可以很容易就获得了理想的结果而关于 synchronized 关键字的内存模型可以这样来表示:
某一个線程在访问一个被 synchronized 修饰的变量时,会对此变量的共享内存进行加锁那么这个时候其他线程对其的访问就会被互斥。 synchronized 的内部实现其实也是鎖的概念


  • 《Java多线程会出现什么问题编程核心技术》
  • 《Java并发编程的艺术》

我要回帖

更多关于 多线程会出现什么问题 的文章

 

随机推荐