java多线程基础(二)

简介

本次主要介绍java多线程中的同步,也就是如何在java语言中写出线程安全的程序。如何在java语言中解决非线程安全的相关问题。本次着重介绍如下的技术点:

  • 1.synchronized对象监视器的使用
  • 2.非线程安全问题是如何出现的
  • 3.关键字 volatile的主要作用
  • 4.volatile与synchroized的区别

synchronized关键字的使用

synchronized实例化的线程安全

如果多个线程共同访问同一个对象中的变量,则有可能出现“非线程安全”的问题。用线程访问的对象中如果有多个实例变量,则运行的结果可能出现交叉的情况。下面以一个例子来进行说明如何利用synchroized来避免这种情况的发生。
首先,我们通过一个例子来看线程之间的变量存在怎样的安全问题:
safeThread代码如下:

public class safeThread {
int num= 0;//共享变量
public void addI(String username){
try {
if(username.equals("a")){
num =100;
System.out.println("a set over!");
Thread.sleep(2000);
}else{
num = 200;
System.out.println("b set over!");
}
System.out.println("username= "+username+" num= "+num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

这里值得注意的是,这里的num变量是被当作共享变量的。下面是ThreadA的代码:

public class ThreadA extends Thread{
private safeThread thread ;
public ThreadA(safeThread thread){
super();
this.thread= thread;
}
public void run(){
super.run();
thread.addI("a");
}
}

ThreadB的代码基本上和ThreadA的相同,只是在run中,执行的是thread.addI("b");,具体代码就不详细给出了。
运行类;

public class runTest {
public static void main(String[] args) {
safeThread num = new safeThread();
ThreadA threada = new ThreadA(num);
ThreadB threadb = new ThreadB(num);
threada.start();
threadb.start();
}
}

运行结果如图:

java多线程基础(二)_第1张图片
Paste_Image.png

就本程序而言,我们希望得到的结果是a将num设置为100,b将num设置为200.而最后得到的结果却不是我们所预想得到的结果。那么怎么修改才能达到我们想要的效果呢?
首先我们得需要先分析为什么会得到两个200的结果,解释就是threadb将前面threada修改过的num又重新修改成200了,所以出现了两个200.针对出现的结果,我们有两种解决方案。一种就是将num设置成线程的私有变量。另一种就是通过添加synchroized关键字来进行控制。
方法一:将num设置成私有变量
将safeThread代码修改如下,其实就是换了以下num定义的位置:


public class safeThread {

public void addI(String username){
int num= 0;
try {
if(username.equals("a")){
num =100;
System.out.println("a set over!");
Thread.sleep(2000);
}else{
num = 200;
System.out.println("b set over!");
}
System.out.println("username= "+username+" num= "+num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

方法二:添加synchroized来进行加锁控制,将safeThread代码修改如下,其实就是在addI前面加个关键字synchroized。

public class safeThread {
int num= 0;
synchronized public void addI(String username){

try {
if(username.equals("a")){
num =100;
System.out.println("a set over!");
Thread.sleep(2000);
}else{
num = 200;
System.out.println("b set over!");
}
System.out.println("username= "+username+" num= "+num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

修改后两个的结果如图:

java多线程基础(二)_第2张图片
方法一的结果.png
java多线程基础(二)_第3张图片
方法二的结果.png

两种修改都达到了我们预想的效果。synchroized关键字在这里的作用就是将AddI方法块添加了一把锁,线程a还没有使用完,线程b需要访问时,必须排队等候,等待a使用完毕才能使用AddI的方法。这就是synchroized关键字的一种简单的用法,为一个方法加锁。同时,通过我们上面的例子,我们可以得出一个结论就是:在两个线程访问同一个对象中的同步方法一定是线程安全的。

如果针对上面方法二的代码,即添加了synchroized关键字的addI方法,修改其运行类的代码如下;

public class runTest {
public static void main(String[] args) {
safeThread nu1 = new safeThread();
safeThread num2 = new safeThread();
ThreadA threada = new ThreadA(nu1);
ThreadB threadb = new ThreadB(num2);
threada.start();
threadb.start();
}
}

运行结果如图;

java多线程基础(二)_第4张图片
Paste_Image.png

那么这个写,与前面的写法究竟有什么区别?上面的示例中是两个线程分别访问同一个类的两个不同实例的相同名称的同步方法,效果却是以异步的方式进行的。本示例由于创建了两个业务对象,在系统中产生了两个锁,所以运行结果是异步的,打印的结果就是先打印b,然后打印a。
从上面程序运行的结果来看,虽然上例中使用了synchroized关键字,但打印的顺序却不是同步的,是交叉的,为什么会出现这样的结果?
关键字synchroized取得的锁都是对象锁,而不是把一段代码或者方法当作锁,所以在上面的实例中,那个线程先执行代synchroized关键字的方法,那个线程就先占有了该方法所属对象的锁Lock,那么其他线程只能呈等待状态,前提是多个线程访问的是同一个对象。
但如果多线线程访问多个对象,则JVM会创建多个锁,本示例就是创建了两个类的对象,所以就会产生了两个锁。
调用用关键字synchroized声明的方法一定是排队运行的。另外需要牢牢记住“共享”这两个字,有共享资源的读写访问才需要同步化,如果不是共享资源,那么根本没有同步的必要。
为了加深对synchroized关键锁的是对象,创建下面实例进行说明;
MyObject.java文件代码如下:

public class MyObject {
public void methord() {
try {
System.out.println("开始运行,线程名" + Thread.currentThread().getName());
Thread.sleep(2000);
System.out
.println("线程 " + Thread.currentThread().getName() + " 结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

自定义线程类ThreadA.java和ThreadB.jave。

public class ThreadA extends Thread {
private MyObject myObject;
public ThreadA(MyObject object){
this.myObject =object;
}

public void run(){
myObject.methord();
}
}

public class ThreadB extends Thread {
private MyObject myObject;
public ThreadB(MyObject object){
this.myObject =object;
}

public void run(){
myObject.methord();
}
}

类Runtest代码如下:

public class Runtest {

public static void main(String[] args) {
MyObject myObject = new MyObject();
ThreadA a = new ThreadA(myObject);
ThreadB b = new ThreadB(myObject);
a.setName("A");
b.setName("B");
a.start();
b.start();
}
}

程序运行效果如下图,更改MyObject.java代码如下(添加synchroized关键字):

public class MyObject {
synchronized    public void methord() {
try {
System.out.println("开始运行,线程名" + Thread.currentThread().getName());
Thread.sleep(2000);
System.out
.println("线程 " + Thread.currentThread().getName() + " 结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

如上面代码所示,在methord方法前加入关键字synchroized进行同步处理。程序运行效果如下图:

java多线程基础(二)_第5张图片
没有关键字synchroized的运行效果图.png
java多线程基础(二)_第6张图片
有关键字synchroized的运行效果图.png

由此,我们不难得出下面的结论:

  1. A线程先持有object对象的Lock锁,B线程可以以异步的方式调用object对象中的非synchroized类型的方法。
  2. B线程先持有object对象的Lock锁,B线程如果在这时调用object对象中的synchronized类型的方法则需要等待,也就是同步。

虽然在赋值时进行了同步,但在取值的时候可能学坏才能一些意想不到的意外,这种情况就是脏读,发生脏读的情况就是在读取实例变量时,此值已经被其他的线程更改过了。
下面以一个例子来说明脏读是如何出现,以及我们怎么去解决脏读。创建项目,新建PublicVar.java文件代码如下:

public class PublicVar {
private String username="a",password="aa";

public void setAccout(String username,String password) throws InterruptedException{
this.username=username;
Thread.sleep(1000);
this.password = password;
}
synchronized public void getAccout(){
System.out.println("username ="+username+"\n password: "+password);
}
}

同步方法setAccout的锁属于类publicVar的实例。创建线程类ThreadVar的代码如下:

public class ThreadVar extends Thread{
PublicVar var = new PublicVar();

public ThreadVar(PublicVar var){
this.var = var;
}

public void run(){
try {
var.setAccout("b", "bb");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

运行类文件代码如下:

public class runVarTest {
public static void main(String[] args) throws InterruptedException {
PublicVar var = new PublicVar();
ThreadVar thread = new ThreadVar(var);
thread.setName("a");
thread.start();
Thread.sleep(200);
var.getAccout();
}
}

程序运行结果如图:

java多线程基础(二)_第7张图片
Paste_Image.png

出现脏读是因为getAccout()方法并不是同步的,所以可以在任意的时候进行调用,解决方法当然是加上同步synchronized的关键字,代码如下:

public class PublicVar {
private String username="a",password="aa";
synchronized public void setAccout(String username,String password) throws InterruptedException{
this.username=username;
Thread.sleep(1000);
this.password = password;
}
synchronized public void getAccout(){
System.out.println("username ="+username+"\n password: "+password);
}
}

运行结果如图:

Paste_Image.png

通过这个案例不仅知道脏读是通过synchronized关键字解决的,要可以总结出以下的内容

  • 当A线程第啊用angObject对象加入synchronized关键字的X方法时,A线程就获得了X方法锁,更准确的讲,是获得了对象的锁,所以其他线程必须等线程A执行完毕之后才可以调用X方法,但线程B可以随意调用其他的非synchronized同步方法。
  • 当线程A调用anyObject对象加入synchronized关键字的X方法时,A线程就获得了X方法所在的对象锁,所以其他线程必须等待A线程执行完毕才可以调用X方法,而B线程如果调用了声明了synchronized关键字的非X方法时,必须等待A线程将X方法执行完毕,也就是释放对象锁后才可以进行调用。这时A线程毕竟执行完一个完整的任务。也就是说username和password这两个实例变量已经同时被赋值,不存在脏读的基本环境。

synchronized锁重入

“可重入锁”的感念是:自己可以再次获取自己的内部锁,比如有一条线程获得了某个对象的锁,此时这个对象的锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁冲入的话,就会造成死锁。
关键字synchronized拥有锁冲入的功能,也就是在使用synchronized时,当一个线程得到一个对象后,再次请求次对象锁时是可以再次得到该对象的锁的。这也证明了在一个synchronized方法/块的内部调用本类的其他synchronized方法/块时,是永远可以得到锁的。
下面以一个例子来进行说明:创建Methord代码文件如下:

public class Methord {
synchronized public void methord1(){
System.out.println("methord1 begin run");
methord2();
System.out.println("methord1 end -----");
}

synchronized public void methord2(){
System.out.println("methord2 begin run");
methord3();
System.out.println("methord2 end -----");
}

synchronized public void methord3(){
System.out.println("methord3 begin run");
System.out.println("methord3 end -----");
}
}

线程类的代码如下:

public class ThreadTestA implements Runnable{

Methord metord = new Methord();

public ThreadTestA(Methord metord){
this.metord = metord;
}
@Override
public void run() {
this.metord.methord1();
}
}

运行类代码:

public class RunTest {

public static void main(String[] args) {
Methord methord = new Methord();
Runnable runnable1 = new ThreadTestA(methord);

Thread thread = new Thread(runnable1);
thread.start();
}
}

运行结果如图:

java多线程基础(二)_第8张图片
Paste_Image.png

通过这个例子,相信大家都能很清楚的知道可重入锁的含义是什么了。

synchronized的同步与及继承性

接下来要讲的是,可重入锁也支持在父子类继承的环境中。当存在父子类继承关系时,子类是完全可以通过“可重入锁”调用父类的同步方法的。下面我们以例子为证明:

//父类方法
public class Main {
public int i = 10;

synchronized public void domain() {
try {
i--;
System.out.println("main print i= " + i);

Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

//子类方法
public class Sub extends Main {
synchronized public void domain() {
try {
while(i>0){
i--;
System.out.println("sub print i= " + i);
Thread.sleep(100);
super.domain();
}

} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

//线程类

public class MyThread extends Thread{
public void run(){
Sub sub = new Sub();
sub.domain();
}
}

//测试调用类:
public class TestRun {

public static void main(String [] args){
MyThread thread = new MyThread();
thread.start();
}
}

该程序的运行结果如图:

java多线程基础(二)_第9张图片
Paste_Image.png

值得注意的是,同步是不可以继承的。如果需要子类和父类进行同步,需在子类的方法中添加synchronized关键字。
需要注意的一点是,当出现异常时,锁会自动释放。这个比较好理解,就不做例子进行解释。

synchronized同步语句块

前面介绍的都是用关键字synchronized声明方法,但是声明方法也是有弊端的,比如A线程调用的异步方法执行一个长时间的任务,那么B线程就必须等待比较长时间,在这种情况下,可以考虑使用synchronized同步语句块来解决。
当两个并发线程访问同一个对象object中的synchronized同步代码块时,一段时间内只有有一个线程被执行,另一个线程必须等待当前线程执行完这个代码块之后才能执行该代码块。
创建测试实例:

//文件Task代码文件
public class Task {

private String getData1;
private String getData2;

public void doLongTimeTask() {
try {
System.out.println("begin task...");
Thread.sleep(3000);//模拟长时间的运行任务
getData1="a long time task had run and return 1 thread name ="+Thread.currentThread().getName();
getData2="a long time task had run and return 2 thread name ="+Thread.currentThread().getName();
synchronized(this){
System.out.println(getData1);
System.out.println(getData2);
}

System.out.println("task end...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

//文件Comment代码,用于存储时间

public class Common {
public static long beginTime1;
public static long beginTime2;
public static long endTime1;
public static long endTime2;
}

//线程A和线程B
public class MyThreadA extends Thread{

private Task task;
public MyThreadA(Task task){
this.task = task;
}

public void run(){
Common.beginTime1 = System.currentTimeMillis();
task.doLongTimeTask();
Common.endTime1 =System.currentTimeMillis();
}
}

public class MyThreadB extends Thread{

private Task task;
public MyThreadB(Task task){
this.task = task;
}

public void run(){
Common.beginTime2 = System.currentTimeMillis();
task.doLongTimeTask();
Common.endTime2 =System.currentTimeMillis();
}

}

//运行类
public class Runtest {
public static void main(String[] args) {
Task task = new Task();
MyThreadA a = new MyThreadA(task);
MyThreadA b = new MyThreadA(task);
a.start();
b.start();
try{
Thread.sleep(10000);
}catch(Exception e){
System.out.println(e);
}
long beginTime = Common.beginTime1>Common.beginTime2?Common.beginTime1:Common.beginTime2;
long endTime = Common.endTime1>Common.endTime2? Common.endTime1:Common.endTime2;
System.out.println("task use: "+(endTime-beginTime));
}
}

运行结果如图:

java多线程基础(二)_第10张图片
Paste_Image.png

如果我们是synchronized修饰doLongTimeTask方法,则运行时间必定大于等于6s,通过使用同步代码块有效的提高了效率,将时间降低至3s。通过上面的实验可以得知,当一个线程访问object时的一个synchronized同步代码块时,另一个线程仍然可以访问该object对象中的非synchronized(this)同步代码块。
在使用同步synchronized(this)代码块时需注意的是,当一个线程访问object的一个synchronized同步代码块时,其他线程对同一个object中所有其他synchronized(this)同步代码块的访问将被阻塞,这说明synchronized使用的“对象监视器”是一个。和synchronized方法一样,synchronized(this)代码块也是锁定当前对象。下面将介绍将任意对象作为对象监视器。

synchronized对象监视器

多个线程调用同一个对象中的不同名称的synchronized同步方法或者synchronized(this)同步代码块时,调用的效果就是按顺序执行,也就是同步的,阻塞的。
这说明synchronized同步方法或synchronized(this)同步代码块分别有两种作用。

  1. synchronized同步方法
  • 对其他synchronized同步方法或synchronized(this)同步代码块调用呈阻塞状态。
  • 同一时间只有一个线程可以执行synchronized同步方法中的代码。
  1. synchronized(this)同步方法块
  • 对其他synchronized同步方法或synchronized(this)同步代码块调用呈阻塞状态。
  • 同一时间只有一个线程可以执行synchronized(this)同步代码块中的代码。

前面我们都是通过synchronized方法或者synchronized(this)来作为“对象监视器”,其实java还支持“任意对象”作为“对象监视器”。使用格式为:synchronized(angObject)。
根据前面的总结:

  1. 在多线程持有“对象监视器”为同一个对象的前提下,同一时间只有一个线程可以执行synchronized(angObject)同步代码块中的代码。
  2. 当持有“对象监视器”为同一个对象的前提下,同一时间只有一个线程可以执行synchronized(angObject)不同代码块中的代码。
    下面以示例来进行说明:
    Service代码如下:
public class Service {
private String username;
private String password;
private String angString = new String();

public void setUserAndrPassword(String user, String pass) {

try {
synchronized (angString) {
System.out.println("the thread name is "
+ Thread.currentThread().getName() + " at "
+ System.currentTimeMillis() + " be come in");
this.username = user;
Thread.sleep(3000);
System.out.println("the thread name is "
+ Thread.currentThread().getName() + " at "
+ System.currentTimeMillis() + " be come out");

this.password = pass;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

其他的文件代码如下:

//线程A代码文件
public class ThreadA extends Thread{
private Service service;
public ThreadA(Service stervice){
this.service =stervice;
}
public void run(){
service.setUserAndrPassword("a", "aa");
}
}
//线程B的文件代码
public class ThreadB extends Thread{
private Service service;
public ThreadB(Service stervice){
this.service =stervice;
}
public void run(){
service.setUserAndrPassword("b", "bb");
}
}
//运行类
public class RunTest {
public static void main(String[] args) {
Service service = new Service();
ThreadA a = new ThreadA(service);
ThreadB b = new ThreadB(service);
a.setName("A");
b.setName("B");
a.start();
b.start();
}
}

运行结果如图:

java多线程基础(二)_第11张图片
Paste_Image.png

可见,使用“synchronized(anyObject)”格式进行同步操作时,对象监视器必须是同一个对象,如果不是同一个对象监视器,运行的结果就是异步调用了,就会交叉运行。
将Service.java文件代码更改如下(将angString变成私有对象):

public class Service {
private String username;
private String password;

public void setUserAndrPassword(String user, String pass) {
String angString = new String();
try {
synchronized (angString) {
System.out.println("the thread name is "
+ Thread.currentThread().getName() + " at "
+ System.currentTimeMillis() + " be come in");
this.username = user;
Thread.sleep(3000);
System.out.println("the thread name is "
+ Thread.currentThread().getName() + " at "
+ System.currentTimeMillis() + " be come out");

this.password = pass;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

运行的效果如图:

java多线程基础(二)_第12张图片
Paste_Image.png

“synchronized(angObject)”格式的写法是将X对象本身作为“对象监视器”,这样就可以得出以下的三个结论:

  1. 当多个线程同时执行synchronized同步代码块时呈同步效果。
  2. 当其他线程执行x对象中synchronized同步方法时呈同步效果。
  3. 当其他线程执行x对象方法里面的synchronized(this)代码块时也呈现同步效果。但需要注意:如果其他线程调用不加synchronized关键字方法时,还是异步调用。

到了这里,还有几点需要注意的:当“对象监视器”为String类型时,需要注意String的常量池特性。对于String常量池的特性,在次不多做解释,有需要的可以自己查阅相关资料,简单的的介绍如下:

String a="a";
String b ="a";
System.out.println(a==b);

此时a和b的地址是相同的,此时返回的值为true;
所以在对象监视器为string类型时,要时刻注意其常量池的特性。所以通常不用synchronized代码块使用String作为所对象,而改用其他,比如new Object()实例化一个Object,但他并不放入缓存中。
对了,这里还需要提示一下,只要对象不变,即使对象的属性被改变,运行的结果还是同步的。

静态同步synchronized方法与synchronized(class)代码块

关键字synchronized还可以应用在static静态方法上,如果这样写,那就是对当前的*.java文件对应的class类进行持锁,下面就次举例说明:
类文件:

public class Service {
synchronized public static void printA(){
try{
System.out.println("the Thread name is "+Thread.currentThread().getName()+" at :"+System.currentTimeMillis()+" come in ");
Thread.sleep(3000);
System.out.println("the Thread name is "+Thread.currentThread().getName()+" at :"+System.currentTimeMillis()+" come out ");
}catch(InterruptedException e){
e.printStackTrace();
}
}
synchronized public static void printfB(){
System.out.println("the Thread name is "+Thread.currentThread().getName()+" at :"+System.currentTimeMillis()+" come in ");
System.out.println("the Thread name is "+Thread.currentThread().getName()+" at :"+System.currentTimeMillis()+" come out ");
}
}

线程类:

//线程A
public class ThreadA implements Runnable{
@Override
public void run() {
Service.printA();
}
}
//线程B
public class ThreadB implements Runnable{
@Override
public void run() {
Service.printfB();
}
}
//运行测试类
public class RunTest {
public static void main(String[] args) {
Runnable a = new ThreadA();
Runnable b = new ThreadB();
Thread A = new Thread(a);
Thread B = new Thread(b);
A.setName("A");
B.setName("B");
A.start();
B.start();
}
}

运行结果如图:

java多线程基础(二)_第13张图片
Paste_Image.png

从运行效果来看,并没有什么特别之处,都是同步的效果,和将synchronized关键字添加到非static方法上使用的效果是一样的,而实际上还是有本质的区别,synchronized关键字添加到static静态方法上是给class类上锁,相当与synchronized(class),而synchronized关键字加到非static静态方法是给对象上锁。

死锁避免

记得在操作系统中学的死锁的四个必要条件:

  1. 互斥条件
  2. 占有等待
  3. 不可剥夺条件
  4. 循环等待条件
    这里就不做仔细的讲解,反正在设计程序的时候要避免出现死锁。查看是否出现死锁的方法,这里稍微介绍一下:
    进入cmd,进入安装JDK文件夹下的bin目录,执行jps命令,查看当前运行的线程id,然后运行jstack -1 [id]此处的id就是前面查看的id。检测出现死锁,如图所示:

内置类与静态内置类

关于内置类和静态内置类的知识点在这里就不展开介绍了。内置类以形如PrivateClass privateClass = publicClass.new PrivateClass();的形式来实例化。我们这里以一个例子来说明synchronized与内置类的使用。

public class OutClass {
static class Inner{
public void methord1(){
synchronized("suo"){
for(int i=0;i<10;i++){
System.out.println("i= "+i+" "+Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
public synchronized void methord2(){
for(int i=10;i<20;i++){
System.out.println("i= "+i+" "+Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}

运行类代码:

import innerClass.OutClass.Inner;

public class Run {
public static void main(String[] args) {
final Inner inner = new Inner();
Thread t1 = new Thread(new Runnable() {

@Override
public void run() {
inner.methord1();
}
}, "a");

Thread t2 = new Thread(new Runnable() {

@Override
public void run() {
inner.methord2();
}
}, "b");
t1.start();
t2.start();
}
}

运行结果:

java多线程基础(二)_第14张图片
Paste_Image.png

通过不同的锁对象,实现异步内置类方法异步执行。

volatile关键字

关键字volatile的主要作用是使变量在多个线程间可见。
如果不是在多继承的情况下,使用继承Thread类和实现Runnable接口在程序运行的结果上并没有多大的区别。如果一旦出现“多继承”的情况,则用实现Runnable接口的方式来处理多线程的问题就很有必要了。
创建一个测试类:

public class PrintString implements Runnable {
private boolean isContinuePrint = true;
public void setContinuePrint(boolean isContinuePrint) {
this.isContinuePrint = isContinuePrint;
}
public void printMethord(){
try{
while(isContinuePrint){
System.out.println("run methord thread name is "+Thread.currentThread().getName()+" --");
Thread.sleep(1000);
}
System.out.println("it had stop");
}catch(Exception e){
e.printStackTrace();
}
}
@Override
public void run() {
printMethord();
}
}
//运行类代码
public class RunTest {
public static void main(String[] args){
PrintString print = new PrintString();
new Thread(print).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("stop! stop!!!!!!!!!!!");
print.setContinuePrint(false);
}
}

运行结果如图:

java多线程基础(二)_第15张图片
Paste_Image.png

看起来程序并没有什么问题,但当上面的代码运行在-servcer服务器模式模式的64位的JVM中上时,会出现死循环。那么是什么样的原因造成JVM设置成-service就出现死循环?在启动线程时,变量private boolean isContinuePrint存在与公共堆栈及线程的私有堆栈中,在JVM被设置呈-service模式时,为了线程运行的效率,线程一直在私有堆栈中取得isContinuePrint的值是true.而代码print.setContinuePrint(false);虽然被执行,更新的却是公共堆栈中的isContinuePrint变量值false,所以就一直是死循环状态。
解决办法就是使用volatile关键字。用volcatile修饰isContinuePrint变量就可以了。通过使用volatile关键字,强制的从公共内存中读取变量的值,内存的结构如图:

java多线程基础(二)_第16张图片
Paste_Image.png

关键字volatile的作用就是强制从公共堆栈中取得变量的值。而不是从线程私有数据栈中取得变量的值。使用volatile关键字增加了实例变量在多个线程之间的可见性,但是volatile最致命的缺点就是不支持原子性。
volatile的使用比较简单,关于原子性的讨论,读者有兴趣可以自己去查阅相关资料。这里不多做介绍。

synchronized与volatile的比较

1)关键字volatile是县城同步的轻量级实现,所以volatiel性能肯定比synchronized要好,并且volatile只能修饰变量,而synchronized可以修饰方法,已经代码块,在开发中使用synchronized关键字的性能消耗肯定还是可较大的。
2) 多线程访问volatile不会发生阻塞,而synchronized会出现阻塞。
3) volatile能保证数据的可见性,但是不能保证原子性;synchronized可以保证原子性,也可以简洁的可见性。因为他可以将私有内存和公
共内存中的数据做同步。
4) 关键字volatile结局的是变量在多线程之间的可见性,而synchronized关键字解决的是多线程之间访问资源的同步性。

最后介绍如何通过使用synchronized实现线程编程的可见性。
Servicer.java代码如下:

public class Service {
private boolean isContinueRun = true;
public void runMethord(){
while(isContinueRun){
}
System.out.println("it had stop!");
}
public void stopMethord(){
isContinueRun = false;
}
}

线程类的代码如下:

//线程A
public class ThreadA implements Runnable{
private Service service;
public ThreadA(Service service){
this.service = service;
}

public void run(){
service.runMethord();
}
}
//线程B
public class ThreadB implements Runnable{
private Service service;
public ThreadB(Service service){
this.service = service;
}

public void run(){
service.stopMethord();
}
}
//运行类
public class RunTest {

public static void main(String [] args) throws InterruptedException{
Service servcer = new Service();
Runnable a = new ThreadA(servcer);
Runnable b = new ThreadB(servcer);

Thread threada = new Thread(a);
Thread threadb = new Thread(b);
threada.start();
Thread.sleep(1000);
threadb.start();
}
}

如果此时运行,会发现什么现象都没有发生,并没有我们预想中的线程停止的提示信息弹出,这个结果是个线程间数据值没有可视性造成的,而关键字可以具有可视性,修改Service.java代码如下:

public class Service {
private boolean isContinueRun = true;
public void runMethord(){
Object object = new Object();
while(isContinueRun){
synchronized( object ) {
}
}
System.out.println("it had stop!");
}
public void stopMethord(){
isContinueRun = false;
}
}

运行效果如图:

java多线程基础(二)_第17张图片
Paste_Image.png

关键字synchronized可以保证在同一时刻,只有一个线程执行某一个方法或者某个代码块。他包含两个特征:互斥性和可见性。

���K��

你可能感兴趣的:(java多线程基础(二))