关于线程 thread (4)线程的交互

线程交互的基础知识

首先要从java.lang.object的类的三个方法学习
void notify():唤醒此对象监视器上等待的单个线程
void notifyAll(): 唤醒此监视器上等待的所有线程。
void wait():导致当前的线程阻塞等,线程要立马放弃同步代码块被同步对象的锁(目前感觉不合理,可能错了),直到其他线程调用此对象的notify() 或者 notifyAll(). 并且wait的调用对象也得是被锁住的那个对象。(这个是后来发现的规则)

另外 wait还有两个很重要的重载方法:
void wait(long timeout) :导致当前的线程等待,直到其他线程调用此线程此对象的notify()方法或者notifyAll()方法,或者超过指定的时间量。
void wait(long timeout, int nanos): 导致当前的线程等待直到其他线程调用此对象的notify() 或者 notifyAll() 方法,或者超过某个实际时间量,或者其他线程中断当前线程。

线程不能在平常的情况下调用对象上等待或者通知的方法,除非这个线程持有该对象的锁。所以 wait , notify, notifyAll 只能在同步环境中用!

wait, notify, notifyall 都是object 的实例方法。正如每个对象的锁一样,每个对象也有一个线程列表,他们等待来自该信号通知。线程通过执行对象上的wait方法获得这个等待列表,从那时起,他就不再执行任何其他指令,直到调用对象的notify方法为止。如果多个线程在同一对象上等待,则只选择一个线程继续执行。如果没有线程等待,则不采取任何特殊操作。调用它的对象一定要一致。

注意哈,wait 和 notify 搭配使用的话,一定要搞清锁住的对象。

你可以模拟出以下场景,有几个线程给你要数据,但是你这个数据很重要,坚决不允许并发篡改的问题出现,那么你:
改数据可以!!一个一个来!! 于是乎在重点的地方乖乖上了把锁。
然后线程们一个接着一个访问!
但是忽然你发现,数据改着改着,到达了某种临界状态,不能再改啦!除非将数据调整到临界点以下。
那么你只能这样做!
线程1 过来要数据,,你大声的告诉他,数据没了,你等着!我做下处理。处理完你可以从中断处继续执行。
但是你是因为自己的数据不符合他的要求而给他说这些情况的,不代表你当前的数据不符合所有线程要求的条件,,万一在逻辑外等待的某线程,人家要求的数据是你现在正好可以提供的呢??所以你还得想个办法,让别的线程能进来,能给数据就给,不能给数据,就告诉后面来的线程让他们等着!
你忽然想到,线程1还拿着锁呢,这要是歇了还不把锁给你,别的线程永远进不来了。不靠谱。于是你又对他说了句,兄嘚,把你的锁给我。好让后面排队的人进来。
于是线程1把锁归还了,,后面在等待的线程2正好轮到了,要执行代码朝你要数据。但是你发现你目前的数据不足以提供给他。于是你还得
将以上的套路重复一遍。 兄嘚,没数据呀,你先等会,好了知会你一声,把锁给我,下一位请进!
于是线程2也歇着了。线程3进来了,但是它要的数据,恰好是你仅仅能够提供得了的。于是你把仅存的一点数据给了它,没有让他等,让他正常的走完你的整个同步方法。

例子:

package Thread;

public class ThreadWaitDemo {
	public static void main(String[] args) {
		Object s = "";	//为了测试执行wait对象的时候,导致当前线程等待之外,,放弃的那把锁到底是谁的锁,
		MyThread thread = new MyThread(s);
		synchronized (thread) {		//锁住了thread对象的锁
			try {
				System.out.println("开始等待计算");
				thread.start();
//				s.wait();
				thread.wait();
				System.out.println("等待结束, total = " + thread.total);
			} catch (Exception e) {
			}
			
		}
		
	}

}

class MyThread extends Thread {
	Object string = null;
	MyThread(Object s){
		string = s;
	}
	public int total = 0;
	public  void run() {
		synchronized (this) {
			for (int i = 0; i < 101; i++) {
				total += i;
				System.out.println(i+"");
			}
			notify();
//			string.notify();
		}	
	}
}


关于线程 thread (4)线程的交互_第1张图片
关于线程 thread (4)线程的交互_第2张图片
哎,,,写代码的时候,,感觉到坑了!!果然实践很重要,会带出很多思维。什么坑呢??我写的时候,搞不清main 方法里的synchronized 括号里要的那把锁,,和 这一块的同步代码里 thread.wait(),,,为什么是thread调用wait??反正就是一个令当前线程放弃锁的操作,为什么偏偏调用thread的?和 synchronized括号里面的参数难道有什么规矩必须保持一致?还有好多其他的问题,主要集中于 notify 和 wait 之间调用,是不是有一个潜规则,不写就报错。。事实上我就这点试了一下,,的确报错了。 比如,我把main方法里面的thread.wait()方法注掉改为了 s.wait(), 同时把 MyThread 的run方法里面的notify改成了同一个对象的notify(),要凑凑一对,想着反正wait就是暂停当前线程并还线程所在锁的,,即使我用s调出wait(),那也得放弃锁。。运行了一下,可以确定 s.wait() 方法执行了之后,主线程的确放弃了synchronized所持有的那把 thread 的锁,并且执行了 MyThread的run方法,但是但是,,string.notify()执行之后,,就仿佛没了消息!!最后一句“等待结束,,”死活打印不出来。所以推断出 wait 和 notify 肯定有一套规则!但是我这里看不到源码我的天。。咋整。
关于线程 thread (4)线程的交互_第3张图片
好吧,native的。。。
使用wait方法和notify方法用于线程间通信的正确姿势 这篇文章,可以说是条条命中!

嗷嗷我明白了,经过查阅,,wait, notify 这类方法, 所在的同步代码块,锁了谁,就应该用谁调!!!当在对象上调用wait()方法时,执行该代码的线程立即放弃这个线程持有的锁,然而调用notify时,并不意味着这时线程会放弃其锁。如果线程仍然在完成同步代码,则线程在移除之前都不会放弃锁。因此,notify并不意味着这时的锁变得可用。
例子:

/**
 * 要求:用wait 和 notify 确保两个线程一定先递加加后递减! 拒绝使用jion解决
 * @author forev
 *
 */
public class ThreadWaitDemo {
	public static boolean isAddOver = false;	//用于标志递加线程是否执行完毕。
	
	public static void main(String[] args) {
		NumberGame game = new NumberGame();
		game.setAddTimes(100);
		game.setSubtractTimes(100);
		SubtractThread sThread = new SubtractThread(game);
		AddThread aThread = new AddThread(game);
		
		sThread.start();
		aThread.start();
		
	}

}

//递减线程
class SubtractThread extends Thread {
	private NumberGame game= null;
	public SubtractThread(NumberGame g) {
		this.game = g;
	}
	public void run() {
		synchronized (game) {
			try {
				//这里需要有个大大的细节要注意!!!一定要在while循环里面判断,不要用if!不要用if!不要用if!重要的事情说。。
				while (!ThreadWaitDemo.isAddOver){	
					game.wait();
				}
				game.subtract();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			
		}
			
	}
}

//递加线程
class AddThread extends Thread {
	private NumberGame game= null;
	public AddThread(NumberGame g) {
		this.game = g;
	}
	public void run() {
		synchronized (game) {
			game.add();
			game.notify();
			ThreadWaitDemo.isAddOver = true;
		}
			
	}
}

class NumberGame {
	private int total = 0;
	private int addTimes = 0;
	private int subtractTimes = 0;
	
	public void setAddTimes(int addTime) {
		this.addTimes = addTime;
	}
	
	public void setSubtractTimes(int st) {
		this.subtractTimes = st;
	}
	
	public int getTotal(){
		return total;
	}
	
	// 递加  线程安全
	public void add(){
		synchronized (this) {
			for (int i = 0; i <= addTimes; i++) {
				total += i;
				System.out.println("add......" + total);
			}
		}
	}
	//递减 线程安全
	public void subtract() {
		synchronized (this) {
			for (int i = subtractTimes; i >= 0; i--) {
				total -= i;
				System.out.println("subtract......" + total);
			}
		}		
	}
	
}

多次的运行结果:
关于线程 thread (4)线程的交互_第4张图片

多个线程等待一个对象锁时要用notifyAll()

多数情况下,最好通知某个对象的所有线程,如果这样做,可以在对象上使用 notifyAll()让所有在此对象上等待的线程冲出等待区,回到可运行状态。

线程的调度-休眠

Java线程调度是java多线程的核心,只有良好的调度,才能充分发挥系统的性能,提高程序的执行效率。这里要明确一点,不管程序员如何编写调度,只能最大限度的影响执行的次序,而不能做到精准控制。

线程休眠是使线程让出cpu的最简单方法之一,线程休眠的时候,会将CPU资源交给其他的线程,以便能轮换执行,当休眠一定时间之后,线程就会苏醒,进入准备状态等待。例子前面好像有。

线程的调度-合并

线程合并的含义是将几个线程合并为一个单线程执行,应用场景是当一个线程必须等另一个线程执行完毕才能执行时可以使用join方法。前面有例子。

守护线程:

守护线程基本上与普通线程没有啥区别调用线程的setDaemon(true),则可以将其设置为守护线程,守护线程的使用情况比较少,但并非无用,举例来说,JVM的垃圾回收,内存管理等线程都是守护线程。还有就是做数据库应用的时候,使用的是数据库连接池,连接池本身也包含很多后台线程,监控连接的个数,超时时间,状态等。
setDaemon方法详细说明:
public final void setDaemon(boolean on)将线程标记为守护线程,当正在运行的线程都是守护线程的时候,JVM就会退出了。

public class DaemonThreadDemo {
	public static void main(String[] args) {
		
		DaemonThread thread = new DaemonThread();
		thread.setDaemon(true);
		thread.start();
		
		for (int i = 0; i < 20; i++) {
			System.out.println(Thread.currentThread() + " i=" + i);
			try {
			} catch (Exception e) {
				// TODO: handle exception
			}
		}
	}

}

class DaemonThread extends Thread {
	public void run() {
		for (int i = 0; i < 1000; i ++) {
			System.out.println(Thread.currentThread() + " i=" + i);
			try {
			} catch (Exception e) {
				// TODO: handle exception
			}
		}
	}
}

运行结果:
关于线程 thread (4)线程的交互_第5张图片
注意看,,当主线程执行完之后, 守护线程没执行几句就结束了。因为剩下的只有守护线程了,JVM会退出的。

生产者消费者模型

对于多线程的程序来说,不管任何语言,生产者和消费者模型都是最经典的。就像学习每一门语言一样,hello world, 都是最经典的例子。
实际上,准确的来说应该是 生产者-消费者-仓储, 离开了仓储,生产者消费者模型就显得没有说服力了。

  1. 生产者仅仅在仓储未满的时候进行生产,仓满则停止生产。
  2. 消费者仅仅在仓储有产品的时候才会消费,仓空则等待。
  3. 当消费者发现仓储没产品可以消费的时候,就会通知生产者生产
  4. 生产者在生产出可消费产品的时候,应该通知等待的消费者去消费。

例子:

// 模拟生产者消费者模式
public class ProduceConsumeDemo {
	public static void main(String[] args) {
		ZhiZaoChang zhi = new ZhiZaoChang(80);
		ProduceThread produceThread = new ProduceThread(zhi);
		produceThread.setDaemon(true);	//设置为守护线程,当消费者买完了东西,就不生产了
				
		ConsumeThread consume1 = new ConsumeThread(zhi, 50);
		ConsumeThread consume2 = new ConsumeThread(zhi, 80);
		ConsumeThread consume3 = new ConsumeThread(zhi, 60);
		ConsumeThread consume4 = new ConsumeThread(zhi, 10);
		ConsumeThread consume5 = new ConsumeThread(zhi, 20);
		ConsumeThread consume6 = new ConsumeThread(zhi, 90);
		ConsumeThread consume7 = new ConsumeThread(zhi, 50);
		ConsumeThread consume8 = new ConsumeThread(zhi, 90);
		ConsumeThread consume9 = new ConsumeThread(zhi, 40);
		ConsumeThread consume10 = new ConsumeThread(zhi, 35);
		ConsumeThread consume11 = new ConsumeThread(zhi, 99);
		ConsumeThread consume12 = new ConsumeThread(zhi, 79);
		ConsumeThread consume13 = new ConsumeThread(zhi, 45);
		
		produceThread.start();
		
		consume1.start();
		consume2.start();
		consume3.start();
		consume4.start();
		consume5.start();
		consume6.start();
		consume7.start();
		consume8.start();
		consume9.start();
		consume10.start();
		consume11.start();
		consume12.start();
		consume13.start();
	}
}

class ZhiZaoChang{
	public static final int MAX = 100;
	private int mProductNumber = 0;
	
	public ZhiZaoChang(int nowNumber) {
		this.mProductNumber = nowNumber;
	}
	
	public void produce() {
		while (true) {	//加一个死循环,以便于可以随时等待时间片的分配
			synchronized (this) {
				while (mProductNumber < MAX) {
					mProductNumber = MAX; 	//补满货
					System.out.println(Thread.currentThread() + "   生产完了,补满货了");
					notifyAll();
				}
			}
			try {
				Thread.currentThread().sleep(1);	//强制睡眠,以便于切换到其他的线程
			} catch (Exception e) {
				// TODO: handle exception
			}			
		}
	}
	
	public void consumer (int needNumber) {
		synchronized (this) {
			try {
				while (mProductNumber < needNumber) {
					System.out.println(Thread.currentThread() + "   货不够了, 需要消费" + needNumber 
							+ "......余货还有:" + mProductNumber);
					//缺货了,赶紧腾出来锁,让其他的线程用,如果此时恰好有一个其他的消费者获得了这把锁,但是他也发现缺货了,
					//那么它也会走同样的逻辑,把锁甩出,让其他的线程用,如果有一个线程也是消费者线程,但是它购买的量比较少,余货
					//正好够,他就会再消费一波。然后再甩出锁,来回几次剩下的只有都甩出过锁只等notify的消费者线程,和
					//那个生产者线程了,反正总会轮到它!它进行补货,补完就发个通知告诉大家,有货了,继续排队买哈。
					wait();		
				}
				mProductNumber -= needNumber;
				System.out.println(Thread.currentThread() + "   消费完了, 消费了" + needNumber 
						+ "......余货还有:" + mProductNumber);
			} catch (Exception e) {
				// TODO: handle exception
			}	
		}
	}
}

class ConsumeThread extends Thread{
	private ZhiZaoChang zhiZaoChang = null;
	private int needNumber = 0;
	
	public ConsumeThread(ZhiZaoChang zzc, int number) {
		this.zhiZaoChang = zzc;
		this.needNumber = number;
	}
	public void run() {
		zhiZaoChang.consumer(needNumber);
	}
}

class ProduceThread extends Thread {
	private ZhiZaoChang zhiZaoChang = null;
	
	public ProduceThread(ZhiZaoChang zhi) {
		this.zhiZaoChang = zhi;
	}
	
	public void run() {
		zhiZaoChang.produce();
	}
}
	

运行结果:
关于线程 thread (4)线程的交互_第6张图片

关于线程 thread (4)线程的交互_第7张图片
这样下来,所有的消费者线程都要到货了,并且数据还有条不紊。

需要注意的是:notifyAll 起到。的作用就是一个通知作用,不释放锁!得等同步块执行完后才会释放锁,或者粗暴点儿,直接wait,但是上面代码生产者可别用wait哈!所以notifyAll也就是告诉所有线程,你们wait那么久终于有结果了,都醒醒来等锁吧!!

volatile关键字

Java中Volatile关键字详解
这个关键字我认为是有些复杂的。那么耐心的一点一点的看,突破,理解

首先,要知道的是java内存模型中的有序性,原子性,和可见性!

  • 有序性:其实我们上文讲述的同步,实则就是让线程进行有序的访问某一个共有变量。Java 提供了synchronized关键字和volatile关键字用于保证线程之间操作的有序性。volatile其本身就含有 禁止指令重排序 的意思。sychronized是靠一个变量在同一时刻只允许一个线程对其进行lock操作,决定了持有同一对象锁的两个同步块只能是串行执行。
  • 原子性:原子,是世界上最小的单位,本身就意为不可被分割的。例如 a = 0 ,这个是不能再被分割的,是一种原子操作,但是 a++这个操作,是 a = a + 1, a + 1是一步,向a再赋值是另外一步。是可以分割的。所以这样的操作不是一个原子操作。**非原子操作都会存在线程安全问题!!**需要我们使用同步技术将他们变成一个原子操作。一个操作如果是原子操作的话,我们称他有原子性。在 Java 中 synchronized 和在 lock、unlock 中操作保证原子性。我们可以通过阅读API来了解这些原子类的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。
  • 可见性:可见性是一个相对复杂的属性,因为可见性中的错误总是会违背我们的直觉!通常我们无法确定执行读操作的线程能够实时的看到其他线程写入的值,有时候甚至是根本不可能的事情。为了确保线程之间对内存写入操作的可见性,必须使用同步机制!**可见性,指的是线程之间的可见性,一个线程修改的状态对于另外一个线程是可见的!**也就是一个线程修改的结果,对于另一个线程能够立马看到。比如用volatile关键字修饰的变量。就会具有可见性。volatile关键字不允许线程内部缓存和重排序,直接修改内存,所以对其他线程是可见的。但是这里需要注意一个问题,volatile只能让被他修饰的内容具有可见性,但不能确保它具有原子性。比如 volatile int a = 0;之后有一个操作 a++;这个变量a具有可见性,但是a++ 依然是一个非原子操作,也就是这个操作同样存在线程安全问题。

指令重排序:在虚拟机层面,为了尽可能减少内存操作速度远慢于CPU运行速度所带来的CPU空置的影响,虚拟机会按照自己的一些规则(这规则后面再叙述)将程序编写顺序打乱——即写在后面的代码在时间顺序上可能会先执行,而写在前面的代码会后执行

volatile原理

当对非volatile修饰的变量进行读写的时候,每个线程会从内存拷贝变量到CPU缓存中,如果你的计算机具有多个CPU的话。每个线程可能会在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的CPU cache中。
关于线程 thread (4)线程的交互_第8张图片
在访问volatile变量的时候不会执行加锁操作,因此也就不会执行线程阻塞,因此volatile变量是一种比sychronized变量更加轻量级的同步机制。

Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。

当一个变量被volatile关键字所修饰的时候,具有以下特点:

  • 保证此变量对所有的线程的可见性,这里的“可见性”,如本文开头所述,当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。但普通变量做不到这点,普通变量的值在线程间传递均需要通过主内存(详见:Java内存模型)来完成。
  • 禁止指令重排序优化。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障;(什么是指令重排序:是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理)。

Volatile性能:
volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。

注意 volatile关键字解决的仅仅是,,

  1. 去掉线程中对变量的拷贝缓存,使取数据直接在内存中取,避免大家拷贝一份数据,执行的时候乱的不行。
  2. 去掉相关变量赋值操作所在代码区域的重排。

多线程的问题逃不过 有序性,可见性,和原子性这几个问题。思考的时候请多从这三点思考。

你可能感兴趣的:(Java基础)