更新日期: 2020/05/07
- 线程类
- 粗粒度:子线程与子线程之间,和main线程之间缺乏交流
- 细粒度:线程之间有信息交流通讯
- Java通过共享变量达到信息共享
- JDK原生库暂时不支持(点对点的)发送信息(类似MPI并行库直接发送消息)
通过共享变量在多个线程中共享消息
1、static变量(是这个类所有的对象,都共享的一个变量)
注:如果一个线程继承线程类继承Thread类,那么该类的信息共享只能通过static变量实现。 而不是普通的成员变量,普通成员变量达不到信息共享目的
public class ThreadDemo0
{
public static void main(String [] args)
{
new TestThread0().start();
new TestThread0().start();
new TestThread0().start();
new TestThread0().start();
}
}
class TestThread0 extends Thread
{
//private int tickets=100; //每个线程卖100张,没有共享
private static int tickets=100; //static变量是共享的,所有的线程共享
public void run()
{
while(true)
{
if(tickets>0)
{
System.out.println(Thread.currentThread().getName() +
" is selling ticket " + tickets);
tickets = tickets - 1;
}
else
{
break;
}
}
}
}
运行结果显示4个进程共享了100张票,但共享信息不完全一致
private int tickets=100; //每个线程卖100张,没有共享
若不使用static变量,则四个线程分别卖100张票,共400。没有实现线程与线程之间的信息共享目的。
2、 同一个Runnable类的成员变量(在多线程中,实际上是一个拷贝(对象))
public class ThreadDemo1
{
public static void main(String [] args)
{
TestThread1 t=new TestThread1();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
}
}
class TestThread1 implements Runnable
{
private int tickets=100;
public void run()
{
while(true)
{
if(tickets>0)
{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
tickets--;
System.out.println(Thread.currentThread().getName() +" is selling ticket " + tickets);
}
else
{
break;
}
}
}
}
TestThread1只被创建一次,就是t。而new Thread(t)并没有创建TestThread1对象,而是把t包装成线程对象,然后启动。
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
使用的是同一个TestThread1的对象,从而包装内存中tickets变量只有一个。这里new只能new一个Thread的外壳。
3、多线程信息共享问题
- 工作缓存副本(每个线程有自己的工作缓存,当需要数据的时候是从内存中加载完数据,放到工作缓存里面来,然后才开始运算。由于每个线程有自己的工作缓存,若现在有一个线程把工作缓存里面的值修改了,这里已经修改的数据其他线程并不知道。其他线程还是用自己工作缓存里面的数据,进行运算。所以,会导致每一个线程的工作缓存里面的并没有反映出最新的变量是多少,大家用的都是前一刻变量的值,而不是最新的值)
- 关键步骤缺乏加锁限制(当多个线程对同一变量进行修改时候需要进行加锁操作)
- 加锁限制是指关键变量,在修改它的时候,一定要限制一次只能只有一人(线程去修改它);如果多个线程一起修改它就会引起错乱
例子: i++,并非原子性操作
- 读取主存i(正本)到工作缓存(副本)中
- 每个CPU执行(副本)i+1操作
- CPU将结果写入到缓存(副本)中
- 数据从工作缓存(副本)刷到主存(正本)中
4、变量副本问题的解决办法(工作副本可见性)
- 采用volatile关键字修饰变量(关键字volatile作用:一旦一个变量,在工作缓存区里面修改了,其他的线程也能立刻知道)
- 保证不同线程对共享变量操作时的可见性
-volatile变量使用示例:
public class ThreadDemo2
{
public static void main(String args[]) throws Exception
{
TestThread2 t = new TestThread2();
t.start();
Thread.sleep(2000);
t.flag = false;
System.out.println("main thread is exiting");
}
}
class TestThread2 extends Thread
{
//boolean flag = true; //子线程不会停止
volatile boolean flag = true; //用volatile修饰的变量可以及时在各线程里面通知
public void run()
{
int i=0;
while(flag)
{
i++;
}
System.out.println("test thread3 is exiting");
}
}
从运行结果可以看出主线程修改flag为false后,子线程能够接收到
若把代码修改为
boolean flag = true; //子线程不会停止
//volatile boolean flag = true;
则运行结果为
子线程不会接收到主线程对变量的值的修改,会一直运行。
关键步骤加锁限制
- 互斥:某一个线程运行一个代码段(关键区),其他线程不能同时运行这个代码段
- 同步:多个线程的运行,必须按照某一种规定的先后顺序来运行
- 互斥是同步的一种特例(互斥是同步里面最简单的一种方式)
- 互斥关键字synchronized
- synchronized代码块/函数,只能有一个线程进入
- synchronized加大性能负担,但使用简单
public class ThreadDemo3 {
public static void main(String[] args) {
TestThread3 t = new TestThread3();
new Thread(t, "Thread-0").start();
new Thread(t, "Thread-1").start();
new Thread(t, "Thread-2").start();
new Thread(t, "Thread-3").start();
}
}
class TestThread3 implements Runnable {
private volatile int tickets = 100; // 多个 线程在共享的
String str = new String("");
public void run() {
while (true) {
sale();
try {
Thread.sleep(100);
} catch (Exception e) {
System.out.println(e.getMessage());
}
if (tickets <= 0) {
break;
}
}
}
public synchronized void sale() { // 同步函数
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + " is saling ticket " + tickets--);
}
}
}
synchronized加锁,必须加锁某一个对象上。这里定义了一个普通的字符串,只要变量str不为空,都可以把锁放在str上面。意味着线程如果需要执行
sale();
则必须去抢str这把锁,如果抢到了就可以执行。如果没抢到只能等候,等到别人释放str锁,才可以继续往下执行。
synchronized可以修饰代码块,也可以用来修饰函数,加在哪里都可以。
synchronized(str) { //同步代码块
sale();
}
public synchronized void sale() { // 同步函数
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + " is saling ticket " + tickets--);
}
}
选择在代码块或者函数上加锁就可以实现互斥访问。
运行结果