复习
1.使用接口的好处是什么?
2.Java的工作机制?
3.什么是方法重写与方法重载?
4.什么是Java三要素?
5.如何最高效的遍历Map?
6.多态的三个必要条件?
7.HashTable与HashMap的区别?
8.LinkedList与ArrayList的区别?
9.集合框架的继承体系?
10.JDK&JRE&JVM分别是什么以及他们的关系?
前文链接
1.偏头痛杨的Java入门教学系列之认识Java篇
2.偏头痛杨的Java入门教学系列之变量&数据类型篇
3.偏头痛杨的Java入门教学系列之表达式&运算符&关键字&标识符&表达式篇
4.偏头痛杨的Java入门教学系列之初级面向对象篇
5.偏头痛杨的Java入门教学系列之流程控制语句篇
6.偏头痛杨的Java入门教学系列之数组篇
7.偏头痛杨的Java入门教学系列之进阶面向对象篇
8.偏头痛杨的Java入门教学系列之异常篇
9.偏头痛杨的Java入门教学系列之初级集合框架篇
前戏
单线程往往是一条逻辑执行流,从上到下的的执行,如果在执行过程中遇到了阻塞,
那么程序会停止在原地,我们使用debug模式时可以看到这种情况,但单线程的能力有限。
试想一下,如果是tomcat是单线程的话,那么高并发就无从谈起了对吗。
多线程就是多个逻辑执行流在执行,多个执行流之间可以保持独立。
可以理解成:单线程就是餐厅里只有一个服务员,多线程就是餐厅里有多个服务员。
在顾客比较多的时候一个服务员肯定是忙不过来的,那么多线程就派上用场了。
需要注意的是多线程也是需要消耗资源的(例如CPU、内存),是资源换时间的一种做法,
如果服务端的内存和CPU的使用率都很低,那么使用多线程会是一种提升效率的好方法,
是否需要使用多线程、需要多少线程需要自行判断。
此外,多线程是JAVA面试中的重灾区,尤其是线程同步、线程通信、线程池等概念,
因此多线程也是诸位必须通关的游戏。
并行与并发
一些同学容易混淆这两个概念,并发(concurrency)与并行(parallel)是有区别的。
并行是指在同一时刻有多条指令在多个CPU(或多核CPU)上同时执行。
并发是指在同一时刻只能有一条执行在CPU上执行,但多条指令被快速切换执行,
感觉上好像是多个指令在同时执行,但同时执行的指令只有一个。
线程与进程
我们可以简单的理解操作系统(windows、linux、osx)里的一个程序在开始运行(入驻内存)后,
就成为了一个进程。每个运行中的程序就是一个进程,例如:微信、QQ、暴风影音等等。
当一个程序运行时,程序内部可能包含了多个逻辑执行流,每个执行流就是一个线程。
线程与进程是包含的关系。一个进程至少包含一个线程,至多可以包含n个线程,
一个线程必须从属于一个进程。
进程(Process)
进程是操作系统中独立存在的实体,拥有独立的系统资源(内存、私有的地址空间),
在没有允许的情况下,两个进程之间不可以直接通讯。进程之间不共享内存,通信较困难。
进程是程序的一种动态形式,是cpu,内存等资源占用的基本单位,
而线程是不能独立的占有这些资源的。
线程(Thread)
线程是进程中某个单一顺序的控制流,线程是进程的基本执行单位。
当进程被初始化后,主线程就被创建了。线程可以拥有自己的堆栈、计数器、局部变量,
但不拥有系统资源,同一进程下的多个线程共享该进程所拥有的全部资源。
线程的执行是抢占式的,相同进程下的多个线程可以并发执行并相互通信方便,线程之间共享内存。
多线程的使用有效的提高了CPU的利用率从而提升运行效率。
线程的创建与启动
有三种方式创建&启动线程,分别为:Runnable、Callable、Thread。
注意:在执行main()时候,其实main()本身是要启动一个main线程的,也叫主线程。
主线程与我们自己新建的线程是截然不同的线程,请不要混淆。
实现Runnable接口
实现了Runnable接口的类在使用多线程时,可以更方便的访问共享实例变量。
public class RunnableDemo1 {
public static void main(String[] args) {
RunnableDemo r1 = new RunnableDemo();
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r1);
t1.start();
t2.start();
}
}
class RunnableDemo implements Runnable {
int i = 0;
/**
* 线程要执行的代码
*/
public void run() {
for (; i < 10; i++) {
// Thread.currentThread().getName()可以获取当前线程名称
System.out.println(Thread.currentThread().getName() + "==>" + i);
}
}
}
实现Callable接口
jdk1.5才出现的接口,可以视为Runnable的升级版,主要用于方便获取线程的返回值与异常。
public class CallableDemo1 {
public static void main(String[] args) {
//注意Callable需要泛型支持
FutureTask<Boolean> ft1 = new FutureTask<>(new CallableDemo(1));
FutureTask<Boolean> ft2 = new FutureTask<>(new CallableDemo(0));
FutureTask<Boolean> ft3 = new FutureTask<>(new CallableDemo(2));
Thread t1 = new Thread(ft1);
Thread t2 = new Thread(ft2);
Thread t3 = new Thread(ft3);
t1.start();
t2.start();
t3.start();
// 获取线程返回值
try {
//get方法获取返回值时候会导致主线程阻塞,直到call()结束并返回为止
System.out.println(ft1.get());
System.out.println(ft2.get());
System.out.println(ft3.get());
} catch (Exception e) {
//可以catch住线程体里的异常
e.printStackTrace();
}
}
}
class CallableDemo implements Callable<Boolean> {
int flag;
int i = 0;
public CallableDemo(int flag) {
this.flag = flag;
}
/**
* 线程要执行的代码
*/
public Boolean call() throws Exception {
for (; i < 10; i++) {
// Thread.currentThread().getName()可以获取当前线程名称
System.out.println(Thread.currentThread().getName() + "==>" + i);
}
if (flag == 1) {
return true;
} else if(flag == 0){
return false;
}else {
throw new Exception("哇咔咔咔");
}
}
}
继承Thread类
继承Thread就不能继承其他类,而Callable、Runnable接口可以。
继承Thread相对于实现接口而言,不能共享实例变量,使用线程的方法更加方便,
例如:获取线程的id,线程名,线程状态等。
public class ThreadDemo1 {
public static void main(String[] args) {
//创建三个线程对象
ThreadDemo t1 = new ThreadDemo();
ThreadDemo t2 = new ThreadDemo();
ThreadDemo t3 = new ThreadDemo();
//启动三个线程对象
t1.start();
t2.start();
t3.start();
}
}
class ThreadDemo extends Thread{
/**
* 线程要执行的代码
*/
public void run() {
for(int i = 0 ; i < 10 ; i ++) {
//this.getName()可以获取当前线程名称
System.out.println(this.getName()+"==>"+i);
}
}
}
线程状态与生命周期
首先JAVA在JDK1.5之后的Thread类有6种状态(看了JDK源码),网上很多文章写的是5种,
区别在于其中RUNNABLE包含了原来的RUNNABLE和RUNNING,
原来的BLOCKED分解成:BLOCKED、WAITING、TIMED_WAITING。
每个线程在同一时间只能有一种状态,这6种状态是JAVA的线程状态而非操作系统的线程状态。
线程被创建并启动后,不会一直霸占着CPU独自运行,CPU需要在多线程中切换,
线程的执行策略是抢占式的(也依赖于线程优先级),线程状态也会在运行与阻塞中不断切换。
名称 | 描述 |
NEW (新建&初始) | 使用new()创建一个线程后,该线程属于新建状态, 此时初始化成员变量,分配线程所需要的资源,但不会执行线程体。 |
RUNNABLE (运行) | 该状态包含了RUNNING(运行中)与READY(就绪), 调用start()启动线程后,该线程属于可运行状态, 线程的运行需要依赖于CPU的调度,具有随机性, 获得CPU调度后线程状态变成了RUNNING,开始执行线程体。 |
BLOCKED (阻塞) | 当前线程在等待其他线程synchronized锁释放时,会进入阻塞状态。 等到其他线程释放synchronized锁时,当前线程进入可运行状态。 多线程情况下为了保证线程同步会使用synchronized锁机制。 |
WAITING (等待) | 当前线程等待其他线程执行操作。 |
TIMED_WAITING (计时等待) | 当前线程在一定时间范围内等待其他线程执行操作。 |
TERMINATED (终止&死亡) | 线程终止状态,线程终止之后不可再调用start(),否则将抛异常。 |
根据上面的线程状态,就可以推出线程的生命周期,就是一个线程从出生到死亡的过程。
图片参考《Java并发编程的艺术》
线程的控制
JDK通过提供一些方法来控制线程的执行。
join加入
让线程A等线程B完成之后再执行,我们可以在线程A中使用线程B的join(),此时线程A将阻塞,
直到线程B执行完再恢复执行。
我们可以将大问题划分成许多个小问题,再为每个小问题分配一个线程,当所有的小问题都完成后,
再调用主线程来接着往下走。
public class JoinDemo1 {
public static void main(String[] args) throws Exception {
System.out.println("start");
JoinThread r = new JoinThread();
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
t1.start();
t2.start();
t1.join();
t2.join();
//主线程等待t1,t2两个线程都执行完毕再继续往下走。
//此处t1,t2会并发执行,并不会因为t1.join()先调用就执行完t1再执行t2,
//join()方法也可以加入超时时间,如果超过时间则不再等待
System.out.println("done");
}
}
class JoinThread implements Runnable {
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
在这个例子中,如果没有加join(),则在t1,t2两个线程还没有执行完就会打印出done,
因为线程是争抢执行的,主线程开完2个子线程后就直接走到done了,
2个子线程的执行相当于是异步,主线程不会等待t1,t2两个线程执行完毕再执行。
需要注意,在本例中,t1,t2会是并发执行,而非t1走完再走t2。
至于为什么t1,t2是并发执行而不是串行,我想应该作者就是想这么设计的,
因为如果把join设计成串行执行,那效率会大打折扣,相当于没有用到多线程并发。
sleep休眠
Thread.sleep()可以让正在执行的线程暂停若干毫秒,并进入等待状态,时间到了之后自动恢复。
在休眠时间范围内即使当前没有任何可执行的线程,休眠中的线程也不会被执行。
sleep()不释放对象锁,如果当前线程持有synchronized锁并sleep(),则其他线程仍不能访问。
public class DaemonDemo1 {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
if (i == 5) {
try {
//暂停5秒
Thread.sleep(5 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
yield让步
Thread.yield()使当前线程放弃CPU调度,但是不使线程阻塞&等待,即线程仍处于可执行状态,
随时可能再次获取CPU调度。相当于认为当前线程已执行了足够的时间从而转到另一个线程。
也有可能出现刚调用完Thread.yield()放弃CPU调度,当前线程立刻又获得CPU调度。
public class YieldDemo1 {
public static void main(String[] args) {
YieldThread r = new YieldThread();
new Thread(r).start();
new Thread(r).start();
}
}
class YieldThread implements Runnable {
public void run() {
for (int i = 0; i < 100; i++) {
if(i==50) {
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
priority优先级
所谓优先级就是谁先执行谁后执行的问题,每个线程在运行时都具有一定的优先级,
优先级高的线程具有较多的执行机会,优先级低的线程具有较少的执行机会。
每个线程的默认优先级与创建它的父线程的优先级相同。优先级范围只能是1-10之间。
setPriority()可以传入一个正整数作为参数,但一般建议使用Thread的常量来设置优先级:
Thread.MAX_PRIORITY=10
Thread.NORM_PRIORITY=5
Thread.MIN_PRIORITY=1
public class PriorityDemo {
public static void main(String[] args) {
PriorityThread t1 = new PriorityThread();
PriorityThread t2 = new PriorityThread();
t1.setPriority(Thread.MAX_PRIORITY);
t2.setPriority(Thread.MIN_PRIORITY);
t1.start();
t2.start();
System.out.println("得到线程优先级t1="+t1.getPriority());
System.out.println("得到线程优先级t2="+t2.getPriority());
}
}
class PriorityThread extends Thread{
public void run(){
for(int i = 0 ; i < 100 ; i ++){
System.out.println(" "+getName()+":"+i);
}
}
}
suspend挂起
suspend()和resume()配套使用,suspend()使得线程进入阻塞状态,并且不会自动恢复,
必须其对应的resume()被调用,才能使得线程重新进入可执行状态。
因为不建议使用,所以本文不再讲解。
daemon后台&守护线程
后台线程是指在后台运行的线程,为其他线程提供服务,JVM的GC就是后台线程。
如果前台线程全部死亡,后台线程会自动死亡。
前台线程创建的子线程默认是前台线程,后台线程创建的子线程默认是后台线程。
把某线程设置为后台线程的操作必须在线程启动之前,否则会抛异常。
public class DaemonDemo1 {
public static void main(String[] args) {
Thread t1 = new Thread(new DaemonThread());
//设置为后台线程
t1.setDaemon(true);
t1.start();
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
//后台线程还没有完全运行完就会死掉,因为主线程先死了。
//查看是否为后台线程
System.out.println(t1.isDaemon());
System.out.println(Thread.currentThread().isDaemon());
}
}
class DaemonThread implements Runnable{
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
线程的同步
总所周知,多线程是由CPU调度来获取执行且具有随机性与争抢性,
使用多线程访问一个共享对象&数据时,可能造成运算结果异常,数据状态不稳定,
即线程安全问题。那么什么是线程安全与线程不安全呢?
举个栗子:
某商城系统中某件商品的库存是1,代码中判断如果库存=0就不能再购买了,
如果库存>0则能购买,购买成功后,库存减1,在单线程时代这个逻辑是没有问题的。
但如果此时并发来了若干个线程,大家都要去买这个商品,这时运算的结果可能出乎我们的意料。
有可能若干个线程同时进入判断库存>0,都是判断通过,进而大家都购买成功,造成库存为负数。
(库存一致性问题也可以使用其他的解决方案,例如CAS、数据库的锁机制,分布式锁等等)
库存出现负数表示线程不安全,那我们需要使用一种安全的机制,即线程同步机制。
让线程A先执行完把库存扣成0,接着再让线程B执行,以此类推,一个线程执行,其他线程阻塞。
这样库存就不会出现负数了,像有把锁一样,只有获得锁的线程才能执行,其他线程都得阻塞。
这样就是线程安全,线程安全需要使用多线程的同步机制。
线程同步是指多个线程同时访问某资源时,采用一系列的机制以保证同时只有一个线程访问该资源。
线程的同步用于线程共享数据,转换和控制线程的执行,保证内存的一致性。
多线程同步有几种方式实现,会在下面一一列举。
synchronized锁
synchronized关键字可以加在方法上与代码块上,也可叫做synchronized方法与synchronized块。
多线程并发情况下,synchronized关键字能够保证在同一时间只有一个线程执行某段代码,
而其他线程需要等待那个线程执行完毕之后才有机会去执行。
而对于非synchronized关键字修饰的方法和代码块,其他线程均可畅通无阻的执行。
这里有个重要的概念:监视器(monitor)。
JAVA中的每一个对象都有一个监视器,用来检测并发时的重入问题,在非多线程情况下,
监视器不起作用,在synchronized情况下监视器才起作用。
线程开始执行synchronized之前,必须要获得对象的监视器的锁,简称监视器锁。
那么是哪个对象呢?
如果使用synchronized块就是传入参数的对象。
如果使用synchronized实例方法就是this对象。
如果使用synchronized静态方法相当于类本身,该类的所有对象共享一把锁。
(synchronized块相比较而言可以更加精准的控制要加锁的范围,灵活性较高)
在synchronized情况下,任何时刻只能有一个线程可以获取监视器锁,
而其他未获得锁的线程只能阻塞,等到那个线程放弃监视器的锁,这个线程才能获取,进而执行。
被synchronized包含的区域被也被称为临界区,同一时间内只有一个线程处于临界区内,
保证了线程的安全。
例子1
public class SynchronizedDemo0 implements Runnable {
public void run() {
synchronized(this) {
//this代表SynchronizedDemo0对象
for(int i = 0; i<3; i++) {
//模拟执行动作
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
public static void main(String[] args) {
//只new了一个SynchronizedDemo0对象,让三个线程来共享,从而造成同步执行。
SynchronizedDemo0 s = new SynchronizedDemo0();
//新建三个线程
Thread t1 = new Thread(s);
Thread t2 = new Thread(s);
Thread t3 = new Thread(s);
t1.start();
t2.start();
t3.start();
}
}
例子2
public class SynchronizedDemo1 {
public static void main(String[] args) throws Exception {
Item item1 = new Item();
item1.count = 1;
item1.name = "java编程思想";
SynchronizedThread r1 = new SynchronizedThread(item1);
// 开启10个线程来购买商品
for (int i = 0; i < 10; i++) {
new Thread(r1).start();
}
Thread.sleep(1 * 1000);
System.out.println(item1.count);
}
}
/**
* 模拟商品
*/
class Item {
// 商品名称
String name;
// 商品库存
Integer count;
}
/**
* 模拟购买线程
*/
class SynchronizedThread implements Runnable {
private Item item;
public SynchronizedThread(Item item) {
this.item = item;
}
public void run() {
//每个线程都要先获取item对象的监视器的锁,才能进入。
synchronized (item) {
if (item.count > 0) {
System.out.println("购买成功");
item.count--;
} else {
System.out.println("购买失败,库存不足");
}
}
}
}
注意事项:
很多同学只知道加上synchronized代表同步,程序员想不出现库存为负数的情况。
于是就在service层的方法上加synchronized,殊不知这样会带来大问题。
我们一般会把controller、service、dao这三层类的对象设置为单例模式(spring默认),
这就相当于把锁的粒度放在了service层,会导致所有的商品在购买时全部阻塞,造成性能瓶颈。
正确的做法是我们只需要把锁的粒度放在商品对象上即可,即监视器为商品对象,
这样只有并发线程对同一个商品对象操作的时候才会上锁,而不是两个不同的商品来的并发也阻塞。
这样就保证了并发与线程安全,记得要重写商品对象的equals()与hashcode()。
虽然JAVA允许使用任何对象的监视器来获得锁,但我们应该使用可能被并发访问的共享对象。
持有锁的线程执行完同步块代码,锁就释放了,释放出来的锁会被其他线程争抢,
一旦被某线程抢到锁后,没抢到锁的线程只能被阻塞,等待锁释放。
什么时候当前线程会释放监视器的锁?
1.当synchronized块&方法执行完毕;
2.当synchronized块&方法执行中使用break&return跳出来时;
3.当synchronized块&方法执行中遇到exception或error跳出来时;
4.当synchronized块&方法执行中使用了监视器所属对象的wait()时;
什么时候当前线程不会释放监视器的锁?
Thread.sleep()、Thread.yield()、Thread.suspend()。
Lock锁
在JDK1.5开始,JAVA提供了一种更为强大的线程同步机制,通过显式定义同步锁对象来实现同步。
lock锁比synchronized锁的锁定操作更多,lock锁允许更灵活的加锁结构,并支持condition对象。
与synchronized锁相似,每次只能有一个线程对lock对象加锁,
线程访问共享数据前必须先获得lock对象,使用lock对象可以显式的加锁、释放锁。
Lock与ReadWriteLock(读写锁)是JDK1.5提供的两个根基接口,
其中ReadWriteLock允许对共享资源的并发访问。
Lock的实现类是ReentrantLock(可重入锁)。
ReadWriteLock的实现类是ReentrantReadWriteLock(可重入读写锁)。
ReentrantLock(可重入锁)具有可重入性,一个线程可以对已被加锁的ReentrantLock锁再次加锁,
ReentrantLock对象会维持一个计数器来追踪lock()方法的嵌套调用,线程在每次调用lock()加锁后,
必须显式的调用unlock()来释放锁,一段被锁保护的代码可以调用另一段被锁保护的代码。
ThreadLocal
ThreadLocal代表一个线程局部变量,是JAVA为线程安全提供的工具类。
通过把数据放在ThreadLocal中就可以让每个线程创建一个该变量的副本,
每个线程都可以独立的改变其变量副本中的值而不会和其他线程的副本冲突,
好像每一个线程都完全拥有该变量一样。从而避免并发访问的线程安全问题。
ThreadLocal可以简化多线程编程时的并发访问,可以很简洁的隔离多线程的竞争资源。
TheadLocal还可以携带共享资源跨越多个类与方法。
ThreadLocal与其他的同步机制类似,都是为了解决多线程对同一变量的访问冲突,
ThreadLocal不能代替同步机制,维度不一样,
同步机制是通过加锁的机制为了同步多个线程对相同资源的并发访问,在竞争状态下获得共享数据,
而ThreadLocal是为了隔离多个线程的数据共享,从根本上避免多个线程对共享资源的竞争。
此外ThreadLocal还有一个小功能,就是可以在一次线程调用中,可以跨类跨方法携带参数。
public class ThreadLocalDemo1 {
public static void main(String[] args) {
Person p = new Person();
//启动两个线程,两个线程共享person对象
new ThreadDemo1(p).start();
new ThreadDemo2(p).start();
}
}
class ThreadDemo1 extends Thread {
Person p;
public ThreadDemo1(Person p) {
this.p = p;
}
public void run() {
System.out.println(this.getName() + " start " + p.getName());
// 将线程局部变量赋值
p.setName("张三");
System.out.println(this.getName() + " end " + p.getName());
}
}
class ThreadDemo2 extends Thread {
Person p;
public ThreadDemo2(Person p) {
this.p = p;
}
public void run() {
System.out.println(this.getName() + " start " + p.getName());
// 将线程局部变量赋值
p.setName("李四");
System.out.println(this.getName() + " end " + p.getName());
}
}
class Person {
// 定义线程局部变量,每个线程都会保留该变量的一个副本,多个线程之间并不互相印象。
private ThreadLocal<String> name = new ThreadLocal<>();
public String getName() {
return this.name.get();
}
public void setName(String name) {
this.name.set(name);
}
}
volatile
未完待续。。。
deadlock死锁
当两个线程相互等待对方释放锁时就会发生死锁,一旦发生死锁,程序不会发生异常,
也没有提示,所有线程处于阻塞状态,死锁在多锁状态下是很容易触发的。
public class DeadLockDemo1 {
public static void main(String[] args) {
A a = new A();
B b = new B();
/**
* 线程1调用获得b对象的监视器锁,线程被暂停了,但还没有释放。
* 线程2调用并获得a对象的监视器锁,线程被暂停了,但还没有释放。
* 线程1恢复并调用a对象的监视器锁,但a对象的锁被线程2持有还没有释放,因此被阻塞。
* 线程2恢复并调用b对象的监视器锁,但b对象的锁被先吃1持有还没有释放,并且线程1还在等待线程2释放a对象的锁,
* 最后导致两个线程互相等待对方释放锁,因此出现死锁。
*/
//线程1
new Thread(new Runnable() {
public void run() {
//
b.one(a);
}
}).start();
//线程2
new Thread(new Runnable() {
public void run() {
//
a.one(b);
}
}).start();
}
}
class A {
public synchronized void one(B b) {
System.out.println(Thread.currentThread().getName()+" 进入A one");
try {
Thread.sleep(1*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 调用B two");
b.two();
}
public synchronized void two() {
System.out.println("进入A two");
}
}
class B {
public synchronized void one(A a) {
System.out.println(Thread.currentThread().getName()+" 进入B one");
try {
Thread.sleep(1*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 调用A two");
a.two();
}
public synchronized void two() {
System.out.println("B two");
}
}
线程的通信
由于线程的调度与执行存在随机性&争抢性,因此需要一些机制来保证线程的协作运行。
如果现在有存钱、取钱两个线程,要求不断重复存钱取钱操作,存钱后立刻取钱,
不允许有两次连续的存钱,也不允许有两次连续的取钱,那我们就需要这两个线程之间有通信机制。
wait、notify、notifyAll
wait()、notify()、notifyAll()是Object类的三个方法,任意一个对象都可以调用这三个方法,
但是必须在synchronized范围内并已经获取到synchronize锁并需要使用监视器的对象来调用。
wait()
使当前线程进入等待状态并会释放synchronized锁(也可以设置超时时间,超时后自动恢复),
使当前线程进入等待池&等待队列,直到其他线程调用当前监视器对象的notify()或notifyAll()来唤醒。
notify()
唤醒在当前对象监视器上等待的单个线程(唤醒等待队列&等待池中的一个线程),
如果当前有多个线程处于等待状态,则会随机唤醒其中一个线程。
notifyAll()
唤醒在当前对象监视器上等待的所有线程(唤醒等待队列&等待池中的全部线程)。
/**
* 如果现在有存钱、取钱两个线程,要求不断重复存钱取钱操作,存钱后立刻取钱,
* 不允许有两次连续的存钱,也不允许有两次连续的取钱,那我们就需要这两个线程之间有通信机制。
*
*/
public class NotifyDemo1 {
public static void main(String[] args) {
Item item = new Item();
for (int i = 0; i < 10; i++) {
new ThreadGet(item,"get"+i).start();
new ThreadSave(item,"save"+i).start();
}
}
}
/**
* 被两个线程共享的对象所属的类
*/
class Item {
int count = 0 ;
String flag = "save";
}
/**
* 存钱线程
*
*/
class ThreadSave extends Thread {
Item item;
public ThreadSave(Item item,String name) {
super(name);
this.item = item;
}
public void run() {
try {
synchronized (item) {
if ("get".equals(item.flag)) {
item.wait();
}
if ("save".equals(item.flag)) {
item.count++;
System.out.println(Thread.currentThread().getName() + " 存钱后,金额=" + item.count);
item.flag = "get";
item.notifyAll();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 取钱线程
*/
class ThreadGet extends Thread {
Item item;
public ThreadGet(Item item,String name) {
super(name);
this.item = item;
}
public void run() {
try {
synchronized (item) {
if ("save".equals(item.flag)) {
item.wait();
// System.out.println("-------------------"+
// Thread.currentThread().getName() + "取钱被唤醒了,此刻的flag是"+item.flag);
//取钱线程执行后调用item.notifyAll(),notifyAll()会唤醒所有处于item对象等待队列中的所有线程,即所有处于等待状态的存钱线程与取钱线程。
//此刻,如果侥幸让一个取钱线程抢到了锁并执行,就会从item.wait()下面开始执行,在euqals判断时,因为flag=save而导致没有进入到if ("get".equals(item.flag)) 里,
//因此这个线程执行完毕后,没有执行任何操作,那这个线程就相当于浪费掉了,save线程也是同理。
//这就是所谓的“丢线程”
}
if ("get".equals(item.flag)) {
item.count--;
System.out.println(Thread.currentThread().getName() + " 取钱后,金额=" + item.count);
item.flag = "save";
item.notifyAll();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
注意:
当被唤醒的线程继续执行时,会继续执行object.wait()之后的代码,
而不会重新执行一次当前线程。
因此object.wait()的判断应该尽可能写在前面,如果object.wait()写在了代码的最后,
那即使线程被唤醒也是什么都做不了,因为刚被唤醒就执行结束了。
Condition
如果使用Lock锁而非Synchronized锁,就不存在监视器的概念了,
也不能再使用wait()、notify()、notifyAll()来进行线程通信。
取而代之的是使用Condition类来保证线程通信机制,
使用Condition可以让已得到Lock对象却无法继续执行的线程释放Lock对象,
也可以唤醒其他处于等待中的线程。
await(),signal(),await()是Condition类的方法,需要使用Condition对象来调用。
await():类似于wait(),导致当前线程进入等待状态,
直到其他线程调用Condition对象的signal()或signalAll()来唤醒该线程。
signal():类似于notify(),随机唤醒在当前Lock对象上等待的单个线程。
signalAll():类似于notifyAll(),唤醒在当前Lock对象上等待的全部线程。
public class ConditionDemo1 {
public static void main(String[] args) {
Bank b = new Bank();
for (int i = 0; i < 10; i++) {
new Thread(new SaveThread(b)).start();
new Thread(new GetThread(b)).start();
}
}
}
class Bank {
Integer count = 10;
String flag = "save";
final Lock lock = new ReentrantLock();
final Condition condition = lock.newCondition();
public void save() {
try {
lock.lock();
if ("get".equals(flag)) {
condition.await();
} else {
count++;
System.out.println(Thread.currentThread().getName() + " 存钱后,金额=" + count);
flag = "get";
condition.signalAll();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void get() {
try {
lock.lock();
if ("save".equals(flag)) {
condition.await();
} else {
count--;
System.out.println(Thread.currentThread().getName() + " 取钱后,金额=" + count);
flag = "save";
condition.signalAll();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
class SaveThread implements Runnable {
Bank bank;
public SaveThread(Bank bank) {
this.bank = bank;
}
public void run() {
bank.save();
}
}
class GetThread implements Runnable {
Bank bank;
public GetThread(Bank bank) {
this.bank = bank;
}
public void run() {
bank.get();
}
}
BlockingQueue
JDK1.5提供了BlockingQueue阻塞队列接口,当生产者线程试图向BlockingQueue中放入元素时,
如果BlockingQueue已满,则生产者线程被阻塞,当消费者线程试图从BlockingQueue取出元素时,
如果BlockingQueue已空,则消费者线程被阻塞。
两个线程交替向BlockingQueue中放入&取出元素,可以控制线程通信。
BlockingQueue接口有5个实现类:
阻塞队列名称 | 描述 |
ArrayBlockingQueue | 基于数组实现。 |
LinkedBlockingQueue | 基于链表实现。 |
PriorityBlockingQueue | 与PriorityQueue类似,该队列调用remove()、poll()、take()等方法取出 元素时,并不是取出队列中存在时间最长的元素,而是队列中最小的元素, 判断元素的大小根据元素本身大小来自然排序(实现Comparable接口), 也可以使用Comparator进行定制排序。 |
SynchronousQueue | 同步队列,对该队列的存取操作必须交替进行。 |
DelayQueue | 底层基于PriorityBlockingQueue实现,要求元素必须实现Delay接口, 该接口有一个getDelay()方法,DelayQueue根据元素的getDelay()返回值进行排序。 |
public class BlockingQueueDemo {
public static void main(String[] args) throws Exception {
BlockingQueue<String> bq = new ArrayBlockingQueue<>(2);
new Producer(bq).start();
new Producer(bq).start();
new Producer(bq).start();
new Consumer(bq).start();
//生产者必须等待消费者消费后才能继续执行
}
}
/**
* 生产者
*/
class Producer extends Thread{
BlockingQueue<String> bq ;
public Producer(BlockingQueue<String> bq) {
this.bq = bq;
}
public void run() {
for(int i = 0 ; i < 10 ; i++) {
try {
bq.put("a");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println(this.getName()+" 生产完毕"+bq);
}
}
}
}
/**
* 消费者
*/
class Consumer extends Thread{
BlockingQueue<String> bq ;
public Consumer(BlockingQueue<String> bq) {
this.bq = bq;
}
public void run() {
for(int i = 0 ; i < 10 ; i++) {
try {
String take = bq.take();
System.out.println(this.getName()+" 消费完毕 "+take+" "+bq);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
线程组ThreadGroup
线程组可以对一批线程进行分类管理,相当于同时控制这批线程,所有的线程都有指定的线程组,
如果没有显式指定,则会线程属于默认线程组。默认情况下父子线程属于一个线程组。
线程在运行中不能改变所属线程组,线程组允许拥有父线程组。
public class ThreadGroupDemo1 {
public static void main(String[] args) {
//返回主线程所属的线程组
ThreadGroup mainGroup = Thread.currentThread().getThreadGroup();
System.out.println("线程组名称 " + mainGroup.getName());
System.out.println("线程组是否为后台线程 " + mainGroup.isDaemon());
System.out.println("线程组的活动线程数目 " + mainGroup.activeCount());
//创建新的线程组
ThreadGroup newGroup = new ThreadGroup("newGroup");
//设置后台线程
newGroup.setDaemon(true);
//设置线程优先级
newGroup.setMaxPriority(Thread.MAX_PRIORITY);
//设置异常处理
new ThreadDemo(newGroup,"newThread1").start();
new ThreadDemo(newGroup,"newThread2").start();
//中断所有线程
// newGroup.interrupt();
}
}
class ThreadDemo extends Thread {
public ThreadDemo(ThreadGroup threadGroup, String name) {
super(threadGroup, name);
}
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(this.getName() + " " + i);
}
}
}
线程池
新线程的创建成本较高,使用线程池可以提高性能,尤其是需要创建大量生命周期很短的线程时。
此外线程池能够有效的控制并发线程数量(最大线程数)。
从JDK1.5开始支持线程池,主要使用Executors工厂类来创建线程池,有如下方法可以创建:
创建线程池方法 | 描述 |
newCachedThreadPool() | 具有缓存功能的线程池,线程被缓存在线程池中。 返回ExecutorService对象。 |
newFixedThreadPool(int) | 可重用的、具有固定线程数的线程池。 返回ExecutorService对象。 |
newSingleThreadExecutor() | 只有单线程的线程池。 返回ExecutorService对象。 |
newScheduledThreadPool(int) | 指定线程数的线程池,可以指定延迟时间执行线程, 即使线程是空闲的也会保存在线程池中。 返回ScheduledExecutorService对象。 |
newSingleThreadScheduledExecutor() | 只有单线程的线程池,可以指定延迟时间执行线程。 返回ScheduledExecutorService对象。 |
JDK1.7提供了ForkJoinPool来支持将一个大任务分解成多个小任务来进行并行计算,
再把多个小任务的结果合并成总的计算结果。
由于线程池所涵盖的知识点很多,又涉及到concurrent包,因此未完待续。
多线程的应用场景
有很多同学会吐槽说,多线程学完了发现并没有什么用武之地嘛,那可就大错特错了。
我们每天都在接触多线程,只不过是自己不知道而已,例如web服务器的请求就是多线程的。
这里又罗列出一些多线程的场景,以供大家参考。
1.异步处理,可以把占据长时间的程序中的任务放到新线程去处理;
2.定时向大量(例如100万以上)的用户发送邮件&消息&信息;
3.统计分析的业务场景,让每个线程去统计一个部门的某类信息;
4.后台进程,例如GC线程;
5.多线程操作文件,提高程序执行时间;
下面这段引用自网络:
假设有一个请求需要执行3个很缓慢的io操作(比如数据库查询或文件查询),
那么正常的数据可能是:
a.读取文件1(10ms)
b.处理1的数据(1ms)
c.读取文件2(10ms)
d.处理2的数据(1ms)
e.读取文件3(10ms)
f.处理3的数据(1ms)
g.整合1,2,3的数据结果(1ms)
单线程总共需要34ms,但如果你把ab,cd,ef分别分给3个线程去做,就只需要12ms了。
再假设
a.读取文件1(1ms)
b.处理1的数据(1ms)
c.读取文件2(1ms)
d.处理2的数据(1ms)
e.读取文件3(28ms)
f.处理3的数据(1ms)
g.整合1,2,3的数据结果(1ms)
单线程总共需要34ms,如果还是按照上面的划分方案,类似于木桶原理,
速度取决于最慢的那个线程。在这个例子里,第三个线程执行了29ms,
那么最后这个请求的耗时是30ms,比起不用单线程,就节省了4ms,
但有可能线程调度切换也要花个1-2ms,因此这个方案显示的优势就不明显了,
还带来了程序复杂性的提升,不值得。
所以我们要优化文件3的读取速度,可以采用缓存,减少一些重复读取,
假设所有用户都请求这个请求,相当于所有的用户都需要读取文件3,那你想想,
100个人进行了这个请求,相当于你花在读取这个文件上的时间久是28*100 = 2800ms,
如果你把这个文件缓存起来,那只要第一个用户的请求读取了,第二个用户不需要读取了,
从内存读取是很快的,可能1ms都不到。
总结
今天我们学到了一些线程的基本知识,但这些知识才只是刚刚开始,
多线程玩到后面会有很多很深的知识点,包括但不局限于:
java内存模型,锁机制(自旋锁,锁消除,锁粗化,偏向锁,轻量级锁等),concurrent包等等。
并且多线程概念也是面试经常出现的,请大家务必要掌握。
作业
使用3个线程,要求三个线程顺序执行,不允许使用sleep()强制让线程有顺序。
线程A输出1、2、3,
线程B输出4、5、6,
线程C输出7、8、9,
线程A输出10、11、12,
线程B输出13、14、15,
以此类推,一直输出到1000为止。