多线程和并发编程
在String和包装类中都存在着不可变性,由于不可变性就算多个线程同时操作一个变量也不会出现线程安全问题
1. 变量的可见性
每个线程都有自己的工作区(内存),在线程任务执行的时候会从主存中将共享变量的值拷贝一份到自己的工作区中,这样做的好处是可以提高线程的执行效率,同时也带来了一个问题:一个线程对共享数据的修改默认情况下对其他线程来说是不可见的
解决方式有两种:
- 使用同步锁可以保证共享变量在多个线程之间的可见性(重量级的)
- 在变量定义的前面加上一个修饰符volatile,但是该关键字无法保证线程的安全(轻量级)
volatile关键字用法
synchronized关键字,实现线程安全。
1、可见性。多线程访问共享变量时,如果一个线程对其进行修改,其他的线程能够立即发现改变后的值。
2、互斥性。多线程访问共享变量时,如果一个线程对其进行操作(读写),其他线程都会等待其操作结束。
volatile关键字只能保证线程的可见性,不能保证互斥性,所以并不能保证线程安全。
volatile使用在属性前,表示该属性具备线程的可见性。
public class Student extends Thread{
private String name1 = "a";
@Override
public void run() {
System.out.println("线程开始执行");
while(name1.equals("a")) {
}
System.out.println("线程执行完毕");
}
public String getName1() {
return name1;
}
public void setName1(String name1) {
this.name1 = name1;
}
}
public class TestMain6 {
public static void main(String[] args) {
Student student = new Student();
student.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("休眠结束,改变值为b");
student.setName1("b");
}
}
上面的代码会一直卡住,不会结束。
明显修改了属性的值b,按道理应该会执行结束。原因是在线程执行过程中,将属性abc的值复制到多线程中,此时值为a,会一直卡住,当主线程修改值为b时,多线程中并未感知到,因为此时线程比较忙,没有时间片去执行数据同步(更新最新的值),所以一直卡住。
将循环中加入一句打印输出,发现可以正常停止。
while(name1.equals("a")) {
System.out.println("111");
}
以上代码在程序中称为黑魔法(black magic),黑魔法意指在程序中,写的代码作用是A,却无意中执行出了B的效果。
上面的问题提出是因为线程比较忙,导致没有时间片去执行数据同步。其实程序会尽量保证数据的同步(只要有时间去执行),在循环中打印输出,需要对输出对象加锁,需要去获取输出设备进行输出,相对cpu来说,是一个比较闲的事,此时cpu有足够的时间去完成数据的同步。(在对该数据进行操作的时候也会去数据同步)
上面的方式虽然能解决问题,但是不推荐。正确的方式应该使用volatile关键字。
该关键字是每一次访问时都会去获取最新的值。
private volatile String name1 = "a";
2. 线程中的通信
一个JAVA应用中可以同时包含多个线程,每个线程完成自己特定的任务,但是线程之间需要相互进行协作线程之间的通信需要掌握的一些技术手段:
主要是三个方法的学习wait() 等待 notify() 唤醒 notifyAll() 唤醒全部,
以上三个方法都是定义在Object类中,意味了任何一个JAVA对象都能调用这三个方法(因为任何一个对象都能当锁)
IllegalMonitorStateException异常:非法的监视器状态异常,发生这个异常的原因是在调用wait方法时没有获取到对应的锁,我们应该调用锁对象的wait方法,方法的执行效果就是让调用了wait方法的线程进入到等待的状态(类似于之前的sleep)
- wait()方法作用:让当前正在执行的线程到一个队列中去等待
注意:如果没有设置等待的超时时间则无限的等待下去,否则等到指定的时间后自动解除等待- notify()唤醒正在lock锁上等待的线程(随机唤醒所有在某个锁上等待的一个线程)
- notifyAll()唤醒在lock锁上等待的所有线程
使用举例
package com.qianfeng.day17;
public class Test05 {
public static void main(String[] args) {
SynchronizedTest sy = new SynchronizedTest();
//开启6个线程后直接进入等待
for(int i = 1; i <= 6; i++) {
new Thread(new Runnable() {
@Override
public void run() {
sy.waitTest();
}
}).start();
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//开启3个线程唤醒之前的线程
new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0; i < 3; i++) {
sy.notifyTest();
}
}
}).start();
}
}
class SynchronizedTest{
public void waitTest() {
synchronized(this) {
System.out.println(Thread.currentThread().getName() + "线程等待");
try {
this.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "我终于被释放了");
}
}
public void notifyTest() {
synchronized(this) {
System.out.println("线程释放");
this.notify();
}
}
}
案例1:“保护性暂停”模式
具体的步骤:首先提供一个多个线程之间的共享对象GuardedObject,在该对象中对外提供两个方法 get put,在get的方法调用时判断结果是否存在,如果存在则直接返回,如果不存在则进入等待,对于put方法的线程在产生了结果之后需要对等待结果的线程进行唤醒操作
代码举例
package com.qianfeng.day17;
public class Test02ProtectStop {
//回顾volatile的使用
static volatile boolean flag = true;
public static void main(String[] args) {
GuardedObjectes go=new GuardedObjectes();
//1.第一个线程从网上获取一个数据
new Thread(new Runnable() {
@Override
public void run() {
//到远程服务器去下载数据 下载的时间无法确定的
System.out.println("1号线程正在努力的从服务器加载数据...");
try {
//假如这个时间在下载
Thread.sleep(3000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if(flag) {
go.put("hello!");
System.out.println("下载完成!");
} else {
System.out.println("下载失败!");
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
//获取到第一个线程下载的数据才能继续后期的工作
Object data = go.get(2000);
if(data==null) {
System.out.println("等待超时...");
flag = false;
}else {
System.out.println("2号线程获取到数据:" + data);
}
}
}).start();
}
}
class GuardedObjectes{
private Object data=null;
//获取数据
public Object get(long timeout) {
//分两种情况考虑1.如果结果已经存在则直接返回 2.如果结果还没有产生则选择等待
synchronized (this) {
while(data==null) {
try {
System.out.println("还没有下载完成,2号线程进入等待...");
long l1 = System.currentTimeMillis();
this.wait(timeout);//死等
long l2 = System.currentTimeMillis();
if(l2-l1 >= timeout) {
break;
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return data;
}
}
//产生数据
public void put(Object data) {
synchronized (this) {
this.data=data;
this.notifyAll();
}
}
}
案例2:生产者和消费者模式,用来解决特定的需求场景
多个生产者 一个消费者
需要在消费者和生产者线程之间提供一个共享的消息队列
队列具备的特点是:先进先出 ,只能往队列的尾部添加元素,只能从队列的头部插入元素
代码举例
package com.qianfeng.day17;
import java.util.LinkedList;
public class Test03 {
public static void main(String[] args) {
MessageQueues me = new MessageQueues(3);
for(int i = 1; i <= 6; i++) {
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
me.put(""+(int)(Math.random()*100+10));
}
},"线程"+i).start();;
}
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
while(true) {
me.take();
}
}
},"取出线程").start();;
}
}
class MessageQueues{
private LinkedList<String> list = new LinkedList<>();
private int capity;
public MessageQueues(int capity) {
this.capity = capity;
}
public void put(String message) {
//分两种不同的情况进行分别的处理
//1.如果队列已满,生产者线程必须进入等待
//2.队列未满,直接向队列的尾部添加新的消息
Thread cu = Thread.currentThread();
synchronized(list) {
if(list.size() == capity) {
try {
System.out.println(cu.getName() + "队列已满正在等待添加");
list.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
list.addLast(message);
System.out.println(cu.getName() + "成功添加:" + message + "-->" + cu.getName());
list.notify();
}
}
public String take() {
//消费的时候也存在两种情况
//1.如果队列为空则进入等待状态
//2.如果队列种存在消息则从队列的头部删除一个元素并返回
Thread cu = Thread.currentThread();
String message;
synchronized(list) {
if(list.isEmpty()) {
try {
System.out.println(cu.getName() + "队列正空等待添加");
list.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
message = list.removeFirst();
System.out.println("成功取出:" + message + "-->" + cu.getName());
list.notify();
}
return message;
}
}