004-四、网络编程之线程安全详解
一、方法内部变量线程安全
方法内部的变量一般不会有“非线程安全”的问题。示例。
public class TheadTest5 {
public static void main(String[] args) throws InterruptedException {
//定义一个共享实体对象
ThreadEntity entity = new ThreadEntity();
//2个线程共享了一个数据
Runnable myRunnable1 = new MyRunnable1(entity,1);
Runnable myRunnable2 = new MyRunnable1(entity,2);
Thread thread1 = new Thread(myRunnable1);
thread1.start();
Thread thread2 = new Thread(myRunnable2);
thread2.start();
Thread.sleep(2000);
System.out.println("程序结束!!!");
}
}
class MyRunnable1 implements Runnable {
private ThreadEntity threadEntity;
private int number;
public MyRunnable1(ThreadEntity threadEntity,int number) {
this.threadEntity = threadEntity;
this.number = number;
}
@Override
public void run() {
//在线程里调用add方法,测试是否为线程安全
try {
this.threadEntity.add(this.number);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class ThreadEntity {
public void add(int number) throws InterruptedException {
//这里定义了一个举报变量,线程是安全的
final int num;
if (number == 1) {
num = 100;
//这里为了演示效果好一点所以加了一个延时1秒
Thread.sleep(1000);
} else {
num = 200;
}
System.out.println("输入的number为:" + number + ",num="+num);
}
}
//以上代码重点就是定义了一个局部变量,然后在number=1的情况下,休眠了一秒钟。看看结果:
输入的number为:2,num=200
输入的number为:1,num=100
程序结束!!!
二、成员变量非线程安全
还是使用上面的代码测试,只需要将 ThreadEntity
的 add
方法中的 num
变量改成类的成员变量这样就会成为"线程不安全了".测试一下
class ThreadEntity {
/**
* 修改为成员变量
*/
private int num;
public void add(int number) throws InterruptedException {
/* 这里定义了一个局部变量,线程是安全的
final int num;*/
if (number == 1) {
num = 100;
//这里为了演示效果好一点所以加了一个延时1秒
Thread.sleep(1000);
} else {
num = 200;
}
System.out.println("输入的number为:" + number + ",num="+num);
}
}
看看结果:
输入的number为:2,num=200
输入的number为:1,num=200
程序结束!!!
以上2个都输出num=200了,因为这个就是线程不安全造成的,当1线程还未执行到最后一行打印代码,2线程这个时候把num值修改为200了,然后打印了第一条信息,然后1线程就跟着打印第二行信息,这个时候num值已经是200了。
注意:主要是因为2个线程同时操作的同一个对象实例的变量所产生的非线程安全问题的。
三、synchronized
这个关键字前面已经简单描述过了。这里在重新说下。
Synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法。它是一个对象锁。但java严格上说不是对象锁。由于大家都是这么叫的这里也这叫吧。
Synchronized的作用主要有三个:
(1)确保线程互斥的访问同步代码
(2)保证共享变量的修改能够及时可见
(3)有效解决重排序问题。
注意:线程锁碰到异常了就会自动释放锁的。
上一段代码是非线程安全的,现在要变成线程安全的只需要加上这个关键字就好了,测试一下。
/**
* 加上synchronized关键字老同步这个方法就达到线程安全了
* @param number
* @throws InterruptedException
*/
public synchronized void add(int number) throws InterruptedException {
/* 这里定义了一个局部变量,线程是安全的
final int num;*/
if (number == 1) {
num = 100;
//这里为了演示效果好一点所以加了一个延时1秒
Thread.sleep(1000);
} else {
num = 200;
}
System.out.println("输入的number为:" + number + ",num="+num);
}
//在ThreadEntity类的add方法上加上了synchronized关键字就可以将这个add方法变成在线程中同步了。
//结果:
输入的number为:1,num=100
输入的number为:2,num=200
程序结束!!!
同步也是排队运行的,主要是用于有共享数据才会用到同步操作,如果不是没有共享数据就不需要使用同步了。
四、线程的脏读现象
原理和前面的类似,看代码先。
public class TheadTest5 {
public static void main(String[] args) throws InterruptedException {
//定义一个共享实体对象
ThreadEntity entity = new ThreadEntity();
//2个线程共享了一个数据
Runnable myRunnable1 = new MyRunnable1(entity);
Thread thread1 = new Thread(myRunnable1);
thread1.setName("A");
thread1.start();
//为了造成脏读所以在这里需要休眠500毫秒
Thread.sleep(500);
entity.getValue();
System.out.println("程序结束!!!");
}
}
class MyRunnable1 implements Runnable {
private ThreadEntity threadEntity;
public MyRunnable1(ThreadEntity threadEntity) {
this.threadEntity = threadEntity;
}
@Override
public void run() {
try {
this.threadEntity.setValue("我是临时变量num1","我是临时变量num2");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class ThreadEntity {
/**
* 成员变量
*/
private String num1 = "我是成员变量num1";
private String num2 = "我是成员变量num2";
/**
* 加上synchronized关键字老同步这个方法就达到线程安全了
* @param num1
* @param num2
* @throws InterruptedException
*/
public synchronized void setValue(String num1,String num2) throws InterruptedException {
this.num1 = num1;
//为了造成脏读所以在这里需要休眠1秒钟
Thread.sleep(1000);
this.num2 = num2;
System.out.println("setValue:" + Thread.currentThread().getName() + ",num1=" + num1);
System.out.println("setValue:" + Thread.currentThread().getName() + ",num2=" + num2);
}
/**
* 取值
*/
public void getValue(){
System.out.println("getValue:" + Thread.currentThread().getName() + ",this.num1=" + this.num1);
System.out.println("getValue:" + Thread.currentThread().getName() + ",this.num2=" + this.num2);
}
}
解释一下上面的代码,为了制造脏读所以在setvalue方法中为num1和num2中间增加了一个休眠,另外在主方法调用getValue方法的前也休眠了200毫秒,为了就是给num1赋值完成之后休眠,然后主线程getValue就直接运行了,现在看下结果吧。
getValue:main,this.num1=我是临时变量num1
getValue:main,this.num2=我是成员变量num2
程序结束!!!
setValue:A,num1=我是临时变量num1
setValue:A,num2=我是临时变量num2
看出来什么问题了吧,第一行打印的信息和第二行打印的信息。
这样的话解决办法是把getValue方法也同步就可以了。
/**
* 取值
*/
public synchronized void getValue(){
System.out.println("getValue:" + Thread.currentThread().getName() + ",this.num1=" + this.num1);
System.out.println("getValue:" + Thread.currentThread().getName() + ",this.num2=" + this.num2);
}
再次打印结果:
setValue:A,num1=我是临时变量num1
setValue:A,num2=我是临时变量num2
getValue:main,this.num1=我是临时变量num1
getValue:main,this.num2=我是临时变量num2
程序结束!!!
现在就全部一致了!!!
五、synchronized语句块
这个同步语句块比较简单,直接看代码吧!!!
public class TestThread5 {
public static void main(String[] args) {
ThreadEntity threadEntity = new ThreadEntity();
Runnable myRunnable1 = new MyRunnable(threadEntity);
Thread thread1 = new Thread(myRunnable1);
thread1.setName("A");
Runnable myRunnable2 = new MyRunnable(threadEntity);
Thread thread2 = new Thread(myRunnable2);
thread2.setName("B");
thread1.start();
thread2.start();
}
}
class MyRunnable implements Runnable {
private ThreadEntity threadEntity;
public MyRunnable(ThreadEntity threadEntity) {
this.threadEntity = threadEntity;
}
@Override
public void run() {
this.threadEntity.show();
}
}
class ThreadEntity {
public void show() {
try {
/**
* 同步代码块
*/
synchronized (this) {
System.out.println(Thread.currentThread().getName()+",开始时间"+System.currentTimeMillis());
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName()+",结束时间"+System.currentTimeMillis());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//结果
A,开始时间1524235595295
A,结束时间1524235597423
B,开始时间1524235597427
B,结束时间1524235599427
注意:使用synchronized(this)的时候当一个线程访问到某对象中的synchronized(this)或者synchronized代码块时,其它线程对同一个对象的所有synchronized(this)或者synchronized代码块将被阻塞。
再看一个例子。
public class TestThread5 {
public static void main(String[] args) {
ThreadEntity threadEntity = new ThreadEntity();
Runnable myRunnable1 = new MyRunnable(threadEntity,true);
Thread thread1 = new Thread(myRunnable1);
thread1.setName("A");
thread1.start();
Runnable myRunnable2 = new MyRunnable(threadEntity,false);
Thread thread2 = new Thread(myRunnable2);
thread2.setName("B");
thread2.start();
}
}
class MyRunnable implements Runnable {
private ThreadEntity threadEntity;
private boolean flag;
public MyRunnable(ThreadEntity threadEntity,boolean flag) {
this.threadEntity = threadEntity;
this.flag = flag;
}
@Override
public void run() {
//flag:true执行show1方法
if (flag) {
try {
this.threadEntity.show1();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("====================");
}
//flag:true执行show2方法
else {
this.threadEntity.show2();
}
}
}
class ThreadEntity {
public void show1() throws InterruptedException {
//同步锁代码块
synchronized (this) {
System.out.println("show1开始打印"+Thread.currentThread().getName());
//为了显示效果这里休眠2秒
Thread.sleep(2000);
System.out.println("show1结束打印"+Thread.currentThread().getName());
}
}
public void show2() {
//同步锁代码块
synchronized (this) {
System.out.println("show2开始打印"+Thread.currentThread().getName());
System.out.println("show2结束打印"+Thread.currentThread().getName());
}
}
}
//结果:
show1开始打印A
show1结束打印A
====================
show2开始打印B
show2结束打印B
以上可以看出,只要线程执行到了show1方法的synchronized(this)
同步代码块之后,show2
方法的synchronized(this)
同步代码块将被阻塞了。
总结
Java的锁分为对象锁和类锁。
当两个并发线程访问同一个对象 object
中的这个 synchronized(this)
同步代码块时,一个时间内针对该对象的操作只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
- 然而,另一个线程仍然可以访问该
object
中的非synchronized(this)
同步代码块。 -
尤其关键的是,当一个线程访问
object
的一个synchronized(this)
同步代码块时,其他线程对该object
中所有其它synchronized(this)
同步代码块的访问将被阻塞。 -
同步加锁的是对象,而不是代码。因此,如果你的类中有一个同步方法,这个方法可以被两个不同的线程同时执行,只要每个线程自己创建一个的该类的实例即可。
-
不同的对象实例的
synchronized
方法是不相干扰的。也就是说,其它线程照样可以同时访问相同类的另一个对象实例中的synchronized
方法。 -
synchronized
关键字是不能继承的,也就是说,基类的方法synchronized f(){}
在继承类中并不自动是synchronized f(){}
,而是变成了f(){}
。继承类需要你显式的指定它的某个方法为synchronized
方法。
7.对一个全局对象或者类加锁时,对该类的所有对象都起作用。
写完了如果写得有什么问题,希望读者能够给小编留言,也可以点击[此处扫下面二维码关注微信公众号](https://www.ycbbs.vip/?p=28 "此处扫下面二维码关注微信公众号")
看完两件小事
如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:
- 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
- 关注公众号 「方志朋」,公众号后台回复「666」 免费领取我精心整理的进阶资源教程
本文著作权归作者所有,如若转载,请注明出处
转载请注明:文章转载自「 Java极客技术学习 」https://www.javajike.com