Java学习笔记 多线程
0,放在前面
- 不懂的类多点点ctrl+鼠标左键看看源码可能就懂了
1,基本概念
- 进程与线程
- 进程:指的是内存中运行的应用程序,每个进程都有一个独立的内存空间
- 线程:是进程中的一个执行路径,同一个进程中的线程共享一个内存空间,线程之间可以自由切换,并发执行,一个进程至少有一个线程。一个进程启动之后,里面的若干执行路径又可以划分为若干个线程。
- 线程调度
- 分时调度:所有线程轮流使用CPU,平均分配每个线程占用CPU的时间(时间片)
- 抢占式调度:让优先级高的线程先使用CPU,如果优先级相同,则随机选择一个。Java默认抢占式调度。
- 同步与异步
- 同步:排队执行,效率低但安全
- 异步:同时执行,效率高但不安全
- 并发与并行
- 指两个或多个事件在同一个时间段内发生
- 指两个或多个事件在同一时刻发生(同时发生)
2,Java中常见的创建线程的两种方式
-
继承Thread
public class Demo1 { public static void main(String[] args) { //创建线程 MyThread t = new MyThread(); //启动 t.start(); for(int i=0; i<10; i++) System.out.println("world"+i); } //继承Thread的类要重写run方法 static class MyThread extends Thread{ @Override public void run() { for(int i=0; i<10; i++) System.out.println("hello"+i); } } }
-
实现Runnable
public class Demo2 { public static void main(String[] args) { //创建Runnable实例 MyRunnable r = new MyRunnable(); //Thread的众多构造方法中,可以穿一个Runnable对象 Thread t = new Thread(r); t.start(); for(int i=0; i<10; i++) System.out.println("你很捞"+i); } //实现Runnable接口的类也要重写run方法 static class MyRunnable implements Runnable{ @Override public void run() { for(int i=0; i<10; i++) System.out.println("我认为"+i); } } }
-
一般来说,第二种方法更常见,因为java中只能继承一个父类,继承了Thread类就不再能继承其他类了。
-
查看API可以发现,Thread类其实也实现了Runnable接口。但没有重写Runnable接口中的run方法,这个方法留给程序员自己弄。
-
3,Thread类
-
用一段代码学习一下Thread类中的一些常用的方法。
public class Demo3 { public static void main(String[] args) { //使用lambda表达式创建Thread对象 Thread t = new Thread(()-> { for(int i=0; i<10; i++){ System.out.println(Thread.currentThread().getName()+":"+i); //休眠,休眠1s后自动唤醒,会抛中断异常,trycatch处理一下 try { Thread.sleep(1000); } catch (InterruptedException e) { System.out.println("产生中断,线程死亡"); return; } } }); //设置为守护进程,将在所有用户线程执行完毕后自杀,这里用户线程只有main,所以main线程执行完后t将停止打印 t.setDaemon(true); //启动 t.start(); //main线程中执行的代码块儿 for(int i=0; i<5; i++){ System.out.println(Thread.currentThread().getName()+":"+i); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } //线程中断,这里将在main线程执行完后执行中断,执行中断后将抛出InterruptedException异常,转而执行catch代码块中的内容 //t.interrupt(); } }
- 用匿名内部类和lambda表达式创建Thread对象并重写run方法,与main线程做比较。
4,线程安全问题(互斥)
-
考虑这样一个场景,多个机构同时卖票。
public class Demo4 { public static void main(String[] args) { Runnable r = new Ticket(); new Thread(r).start(); new Thread(r).start(); new Thread(r).start(); } static class Ticket implements Runnable{ private static int count = 10; @Override public void run() { while(count>0){ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } count--; System.out.println(Thread.currentThread().getName()+"->余票:"+count); } } } }
- 此时会发生什么?3个机构同时卖票,很容易出现安全问题,会发生逻辑错误,余票会出现-1,-2的情况
- 当count=1时,Thread0刚刚进入while循环,并休眠了1s;此时Thread1抢占了CPU,它也刚刚进入while循环,并休眠了1s;但此时Thread2又抢占了CPU,它不仅进入了while循环,休眠1s后还执行了count–,并打印了语句,此时count=0,Thread2结束运行。
- 但是此时Thread0和Thread1都不知道count=0,他们继续卖票,假设Thread0又获得了时间片,它执行完count–并打印,此时count=-1,结束循环,Thread0结束运行
- Thread1接着获得了时间片,也执行完count–并打印,此时count=-2,结束循环,Thread1结束运行。
如何解决线程安全问题呢?可以将count设置为互斥变量,就行了,可以理解为排队,在一个机构卖票时,其他机构等着,卖完了再抢下一个卖票的机会。换成代码,就是给访问count变量的那段代码上锁。
- 同步代码块(synchronized代码块)
public class Demo5 { public static void main(String[] args) { Runnable r = new Ticket(); new Thread(r).start(); new Thread(r).start(); new Thread(r).start(); } static class Ticket implements Runnable{ private static int count = 10; //声明一个锁对象,只要是对象就可以,作为参数传到synchronized代码块中 private Object o = new Object(); @Override public void run() { while(true) { //声明synchronized代码块,给这段代码加锁,相当于互斥,在操作count时只能有一个线程在运作 synchronized (o) { if(count<=0) break; try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } count--; System.out.println(Thread.currentThread().getName() + "->余票:" + count); } } } } }
- 同步方法(synchronized关键字)
public class Demo6 { public static void main(String[] args) { Runnable r = new Ticket(); new Thread(r).start(); new Thread(r).start(); new Thread(r).start(); } static class Ticket implements Runnable{ private static int count = 10; @Override public void run() { while(true){ if(!sale()) break; } } //将整个sale方法上隐式锁 public synchronized boolean sale(){ if(count > 0) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } count--; System.out.println(Thread.currentThread().getName() + "->余票:" + count); return true; } return false; } } }
- 显示锁Lock
public class Demo7 { public static void main(String[] args) { Runnable r = new Ticket(); new Thread(r).start(); new Thread(r).start(); new Thread(r).start(); } static class Ticket implements Runnable{ private int count = 10; //Lock是个接口,用到了多态的概念 private Lock l = new ReentrantLock(); @Override public void run() { while(count>0){ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //手动上锁 l.lock(); count--; //访问完count后,手动解锁 l.unlock(); System.out.println(Thread.currentThread().getName()+"->余票:"+count); } } } }
5,线程死锁
考虑这样一个场景:绑架,罪犯抓了人质,警察要救人质,要抓罪犯。罪犯的要求是,先放我走,我再放人质,警察说你先放人质,我再放你走。
public class Demo10 {
public static void main(String[] args) {
Culprit c = new Culprit();
Police p = new Police();
Runnable r = new MyThread(p, c);
Thread t = new Thread(r);
t.start();
c.say(p);
}
static class MyThread implements Runnable{
private Culprit c;
private Police p;
MyThread(Police p, Culprit c){
this.p = p;
this.c = c;
}
@Override
public void run() {
p.say(c);
}
}
static class Culprit {
public synchronized void say(Police p){
System.out.println("罪犯:放了我,我就放人质");
p.release();
}
public synchronized void runAway(){
System.out.println("罪犯放了人质,然后逃跑了");
}
}
static class Police {
public synchronized void say(Culprit c){
System.out.println("警察:放了人质,我就放你走");
c.runAway();
}
public synchronized void release(){
System.out.println("警察救了人质,但是罪犯逃跑了");
}
}
}
可以这样理解这段代码:myThread线程调用p.say©时给c上了锁,在这个方法刚刚打印语句,准备执行p.release()的时候,此时main线程抢占CPU,它要调用c.say§,也给p上了锁。打印了语句,准备执行c.runAway()的时候,发现c被上锁了,此时CPU给了myThread,myThread想继续推进自己的程序,但是发现p也被上锁了,于是两边都互相等待对方的临界资源的释放。但是他们永远不会释放,这就造成了死锁。
6,多线程通信问题(生产者与消费者)
考虑这样一个场景:一个厨师,一个服务生,一个盘子,厨师做菜,做完放盘子里,服务生上菜,洗盘子,然后将盘子放到厨师那儿。
public class Demo11{
public static void main(String[] args) {
Food f = new Food();
new Cooker(f).start();
new Waiter(f).start();
}
static class Cooker extends Thread{
private Food f;
public Cooker(Food f){
this.f = f;
}
@Override
public void run() {
for(int i=0; i<100; i++){
if(i%2==0)
f.setNameAndTaste("老干妈", "香辣味");
else
f.setNameAndTaste("煎饼果子", "甜辣味");
}
}
}
static class Waiter extends Thread{
private Food f;
public Waiter(Food f){
this.f = f;
}
@Override
public void run() {
for(int i=0; i<100; i++){
f.get();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
static class Food{
private String name;
private String taste;
public void setNameAndTaste(String name, String taste){
this.name = name;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.taste = taste;
}
public void get(){
System.out.println("服务生端走的菜的名称是:" + name + ",味道:"+ taste);
}
}
}
按照最好的情况,厨师没隔0.1s做一道菜,服务生每隔0.1s端一道菜,交替执行,就不会有错乱了,但是实际上着基本上是不可能的。基本上是这种情况:在厨师上老干妈的时候,刚刚设置名字,还没来得及设置味道,服务生就抢占CPU把菜端走了。所以就出现了甜辣味的老干妈。同理也会出现香辣味的煎饼果子。
如何解决?在厨师做饭的时候,服务生睡觉,厨师做好饭,叫醒服务生,自己睡觉。服务生上菜的时候,让厨师睡觉,洗完盘子,叫醒厨师,自己睡觉。同时,在厨师做饭的时候,上锁,服务生不能操作盘子(此处是flag)。同样,服务生在上菜的时候,初始也不能操作盘子。
public class Demo12{
public static void main(String[] args) {
Food f = new Food();
new Cooker(f).start();
new Waiter(f).start();
}
static class Cooker extends Thread{
private Food f;
public Cooker(Food f){
this.f = f;
}
@Override
public void run() {
for(int i=0; i<100; i++){
if(i%2==0)
f.setNameAndTaste("老干妈", "香辣味");
else
f.setNameAndTaste("煎饼果子", "甜辣味");
}
}
}
static class Waiter extends Thread{
private Food f;
public Waiter(Food f){
this.f = f;
}
@Override
public void run() {
for(int i=0; i<100; i++){
f.get();
}
}
}
static class Food{
private String name;
private String taste;
//flag=true表示可以做菜
private boolean flag=true;
public synchronized void setNameAndTaste(String name, String taste){
if(flag){
this.name = name;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.taste = taste;
//做完菜,叫醒服务生,自己睡觉
flag = false;
this.notifyAll();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void get() {
if (!flag) {
System.out.println("服务生端走的菜的名称是:" + name + ",味道:" + taste);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//上完菜,叫醒厨师,自己睡觉
flag = true;
this.notifyAll();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
7,线程的6种状态
- NEW:线程刚被创建,但没启动
- RUNNABLE:执行中的线程
- BLOCKED:被阻塞的线程
- WAITING:无限期等待里另一个线程执行操作
- TIMED_WAITING:指定时间等待另一个线程执行操作
- TERMINATED:已退出的线程
8,Callable(创建线程的另一种方法)
-
Runnable 与 Callable
接口定义 //Callable接口 public interface Callable<V> { V call() throws Exception; } //Runnable接口 public interface Runnable { public abstract void run(); }
-
Callable****使用步骤
1. 编写类实现Callable接口 , 实现call方法 class XXX implements Callable<T> { @Override public <T> call() throws Exception { return T; } } 2. 创建FutureTask对象 , 并传入第一步编写的Callable类对象 FutureTask<Integer> future = new FutureTask<>(callable); 3. 通过Thread,启动线程 new Thread(future).start();
-
与Runnable的相同点
- 都是接口
- 都可以编写多线程程序
- 都采用Thread.start()启动线程
- 与Runnable的不同点
-
Runnable没有返回值,Callable可以返回执行结果
-
Callable接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主线程继续进行,转而等待子线程执行完毕。
-
举个栗子
import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; public class Demo13 { public static void main(String[] args) { Callable<Integer> c = new MyCallable(); FutureTask<Integer> f = new FutureTask<>(c); new Thread(f).start(); for(int i=0; i<10; i++) { System.out.println(Thread.currentThread().getName() + ":" + i); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } static class MyCallable implements Callable<Integer>{ @Override public Integer call() throws Exception { for(int i=0; i<10; i++) { System.out.println(Thread.currentThread().getName() + ":" + i); Thread.sleep(1000); } return 100; } } }
和Runnable运行效果没什么区别,只不过多了个返回值
但是可以调用get()方法获得返回值,这样主线程main就会等待MyCallable线程执行完毕后,获得其返回值,再执行。
import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; public class Demo14 { public static void main(String[] args) { Callable<Integer> c = new MyCallable(); FutureTask<Integer> f = new FutureTask<>(c); new Thread(f).start(); try { Integer integer = f.get(); System.out.println(integer); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } for(int i=0; i<10; i++) { System.out.println(Thread.currentThread().getName() + ":" + i); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } static class MyCallable implements Callable<Integer>{ @Override public Integer call() throws Exception { for(int i=0; i<10; i++) { System.out.println(Thread.currentThread().getName() + ":" + i); Thread.sleep(1000); } return 100; } } }
9,线程池
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程 就会大大降低 系统的效率,因为频繁创建线程和销毁线程需要时间. 线程池就是一个容纳多个线程的容 器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量的时间和资源。
线程池的好处:
1. 降低资源消耗
2. 提高响应速度
3. 提高线程的可管理性
- Java中的4中线程池
-
缓存线程池
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class Demo15 { public static void main(String[] args) { /** * 缓存线程池长度无限制 * 执行流程: * 1. 判断线程池是否存在空闲线程 * 2. 存在则使用 * 3. 不存在则创建线程,放入线程池,并使用 */ ExecutorService service = Executors.newCachedThreadPool(); service.execute(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()); } }); service.execute(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()); } }); service.execute(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()); } }); service.execute(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()); } }); } }
-
定长线程池
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class Demo16 { public static void main(String[] args) { /** * 定长线程池(指定长度) * 执行流程: * 1. 判断线程池是否有空闲线程 * 2. 存在则使用 * 3. 不存在空闲线程,且线程池为满,则创建线程并放入线程池,然后使用 * 4. 不存在空闲线程,且线程池已满,则等待,直到线程池存在空闲线程 */ ExecutorService service = Executors.newFixedThreadPool(2); service.execute(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()); } }); service.execute(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()); } }); service.execute(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()); } }); } }
-
单线程线程池
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class Demo17 { public static void main(String[] args) { /** * 单线程线程池(定长为1) * 其他与定长线程池相同 */ ExecutorService service = Executors.newSingleThreadExecutor(); service.execute(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()); } }); service.execute(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()); } }); service.execute(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()); } }); } }
-
周期性任务定长线程池
import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; public class Demo18 { public static void main(String[] args) { /** * 周期任务定长线程池 * 定长执行流程: * 1. 判断线程池是否存在空闲线程 * 2. 存在则使用 * 3. 不存在空闲线程,且线程池未满的情况下,创建线程,并放入线程池,再使用 * 4. 不存在空闲线程,且线程池未满的情况下,等待,直到线程池存在空闲线程 * 周期性执行流程: * 定时执行,当某个时机被触发时,自动执行某任务 */ ScheduledExecutorService service = Executors.newScheduledThreadPool(2); /** * 定时执行: * 1. 参数1: Runnable类型的任务 * 2. 参数2: 延迟执行的时长数字 * 3. 参数3: 时长数字的单位 */ service.schedule(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()); } }, 5, TimeUnit.SECONDS); /** * 周期执行: * 1. 参数1: Runnable类型任务 * 2. 参数2: 延迟执行的时长数字 * 3. 参数3: 周期时长(每次执行的间隔时间) * 3. 参数3: 时长数字的单位 */ service.scheduleAtFixedRate(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()); } },5, 2, TimeUnit.SECONDS); } }