Java深入学习(1):多线程详解
多线程目的:在同一时刻有多条不同路径执行程序,提高程序运行效率
多线程应用:数据库连接池,多线程文件下载等
注意:在文件下载中使用多线程,无法提高速度
在一个进程中,一定会有主线程
从基础开始,多线程的使用方式:
1.继承thread类:(不推荐)
public class threaddemo extends thread { @override public void run() { //写入线程执行的代码 } public static void main(string[] args) { threaddemo threaddemo = new threaddemo(); threaddemo.start(); } }
注意:threaddemo调用的是start方法;如果调用了run方法,本质上还是单线程
2.实现runnable接口:
public class threaddemo implements runnable { @override public void run() { //写入线程执行的代码 system.out.println("demo"); } public static void main(string[] args) { threaddemo threaddemo = new threaddemo(); new thread(threaddemo).start(); } }
3.匿名内部类
public class threaddemo { public static void main(string[] args) { new thread() { @override public void run() { //写入线程执行的代码 } }.start(); } }
java8可以简写为这样
public class threaddemo { public static void main(string[] args) { new thread(() -> { //写入线程执行的代码 }).start(); } }
多线程的状态:
1.新建状态:调用start方法之前
2.就绪状态:调用start方法,等待cpu分配执行权
3.运行状态:执行run方法中的代码
4.死亡状态:run方法执行完毕
5.阻塞状态:调用wait或sleep方法,线程变为阻塞状态,阻塞状态可以直接变成就绪状态
守护线程:
在java程序中,有主线程和gc线程(用于回收垃圾),主线程死亡后,gc线程也会死亡,同时销毁
这种和主线程一起销毁的线程就是守护线程
非守护线程:线程的状态和主线程无关
用户线程:以上的三种方式创建的都是用户现场,由主线程创建,也是非守护线程
示例:
public class threaddemo { public static void main(string[] args) { new thread(() -> { for (int i = 0; i < 30; i++) { try { thread.sleep(300); system.out.println("子线程i:" + i); } catch (interruptedexception e) { e.printstacktrace(); } } }).start(); for (int i = 0; i < 5; i++) { system.out.println("主线程i:" + i); } system.out.println("主线程执行完毕"); } }
观察输出发现:打印主线程执行完毕之后,还在继续打印子线程执行信息
只需要对子线程进行设置,即可变成守护线程:
public class threaddemo { public static void main(string[] args) { thread thread = new thread(() -> { for (int i = 0; i < 1000; i++) { system.out.println("子线程i:" + i); } }); thread.setdaemon(true); thread.start(); for (int i = 0; i < 10; i++) { system.out.println("主线程i:" + i); } system.out.println("主线程执行完毕"); } }
观察输出发现:子线程还未打印到999,程序已经结束
join方法:
a线程调用了b线程的join方法,那么a等待b执行完毕之后再执行(a释放cpu执行权)
示例:主线程让子线程执行完毕再执行
public class threaddemo { public static void main(string[] args) throws interruptedexception { thread thread = new thread(() -> { for (int i = 0; i < 60; i++) { system.out.println("子线程i:" + i); } }); thread.start(); thread.join(); for (int i = 0; i < 10; i++) { system.out.println("主线程i:" + i); } system.out.println("主线程执行完毕"); } }
观察输出发现:子线程打印完59,才开始主线程的打印
线程安全问题:
当多个线程共享同一个全局变量,做写的操作时候,会发生线程安全问题
模拟线程安全问题:车站卖票经典案例
public class threaddemo implements runnable { //一共有一百张票 private int count = 100; @override public void run() { while (count > 0) { try { thread.sleep(100); sale(); } catch (interruptedexception e) { e.printstacktrace(); } } } private void sale() { if (count > 0) { system.out.println(thread.currentthread().getname() + "出售第" + (100 - count + 1) + "张票"); count--; } } public static void main(string[] args) { threaddemo threaddemo = new threaddemo(); thread t1 = new thread(threaddemo, "窗口1"); thread t2 = new thread(threaddemo, "窗口2"); t1.start(); t2.start(); } }
观察输出发现:很多票重复出售
线程安全问题解决:
1.在sale方法上使用synchronized关键字
原理:当线程进入该方法时候会自动获取锁,一旦某线程获取了锁,其他线程就会等待,等到执行完毕该线程代码,释放锁
缺点:降低程序效率,每次执行该方法都需要进行判断
private synchronized void sale() { if (count > 0) { system.out.println(thread.currentthread().getname() + "出售第" + (100 - count + 1) + "张票"); count--; } }
2.使用同步代码块
public class threaddemo implements runnable { //一共有一百张票 private int count = 100; private final object object = new object(); @override public void run() { while (count > 0) { try { thread.sleep(100); sale(); } catch (interruptedexception e) { e.printstacktrace(); } } } private void sale() { synchronized (object) { if (count > 0) { system.out.println(thread.currentthread().getname() + "出售第" + (100 - count + 1) + "张票"); count--; } } } public static void main(string[] args) { threaddemo threaddemo = new threaddemo(); thread t1 = new thread(threaddemo, "窗口1"); thread t2 = new thread(threaddemo, "窗口2"); t1.start(); t2.start(); } }
观察输出:问题解决
注意:如果写成这样还是存在问题
public static void main(string[] args) { threaddemo threaddemo1 = new threaddemo(); threaddemo threaddemo2 = new threaddemo(); thread t1 = new thread(threaddemo1, "窗口1"); thread t2 = new thread(threaddemo2, "窗口2"); t1.start(); t2.start(); }
这时候需要给全局变量加上static关键字:共享同一个锁
private static int count = 100; private static final object object = new object();
观察输出:问题解决
多线程死锁问题:
产生场景:初学者喜欢每个地方都加入synchronized,于是synchronized中嵌套synchronized,容易产生死锁
产生原因:a线程拿到了锁2,现在需要拿锁1;b线程拿了锁1,现在需要拿锁2;a线程拿不到锁1就不会释放锁2;b线程拿不到锁2就不会释放锁1
threadlocal类:
什么是threadlocal:给每一个线程提供局部变量
原理:底层是一个map集合,获取当前线程,然后调用map的put和get方法实现
初始化:
public static threadlocal<integer> threadlocal = threadlocal.withinitial(() -> 0);
获取:
threadlocal.get();
设置:
threadlocal.set(count);
多线程特性:
1.原子性
2.可见性
3.有序性
java内存模型(jmm):
jmm决定一个线程对共享变量的写入时,能够另一个线程是否可见
主内存:共享存储的变量
本地内存:共享变量的副本
线程安全问题根本原理:共享变量存放于主内存中,每一个线程都有本地内存。比如我在主内存中存入count=100,那么两个本地内存都存放了count=100副本。这时候两个线程同时操作共享变量count-1,首先两个线程要现在本地内存进行count-1操作,然后刷新到主内存。于是,出现了线程安全问题!
volatile关键字:
一个示例:
class threadtest extends thread { public boolean flag = true; @override public void run() { system.out.println("线程开始"); while (flag) { } system.out.println("线程结束"); } public void setrunning(boolean flag) { this.flag = flag; } } public class threaddemo { public static void main(string[] args) throws interruptedexception { threadtest threadtest = new threadtest(); threadtest.start(); thread.sleep(3000); threadtest.setrunning(false); system.out.println("flag改为false"); thread.sleep(3000); system.out.println("flag:" + threadtest.flag); } }
打印如下:
线程开始 flag改为false flag:false
然后程序卡死
为什么已经把flag改为false,子线程还是走入了while循环
因为:主线程把flag改了,还没有刷入主内存,子线程一直在读本地内存中的变量
解决:只需要加入volatile关键字
作用:将修改的值立即更新到主内存,保证其他线程对该变量的可见
public volatile boolean flag = true;
打印如下:
线程开始 flag改为false 线程结束 flag:false
注意:volatile只能保证可见性,不能保证线程安全
使用场景:观察主流框架,可以发现只要是全局共享的变量,都加入了volatile关键字
synchronized与volatile关键字区别:
volatile保证可见性,不能保证原子性,也就是不能保证线程安全,禁止重排序
synchronized既可以保证原子性,也可以保证线程安全,不禁止重排序
重排序:
概念:cpu会对代码实现优化,不会对有依赖关系性做重排序
什么是依赖关系:
int a = 1; int b = 2; int c = a + b;
c依赖a,b。c和a,b都有关系。c一定在a,b之后执行,而a,b执行顺序不一定
所以在代码执行时候,可能先执行的是int b = 2而不是int a = 1
但是在这里执行的结果不会发生改变
注意:一般只会在多线程中遇到重排序问题
重排序问题的解决:加入volatile关键字
线程之间的通信:
多个线程在处理同一个资源,但是线程的任务却不相同,通过一定的手段使各个线程能有效地利用资源,
这种手段即:等待唤醒机制,又称作线程之间的通信
涉及到的方法:wait(),notify()
示例:
两个线程一个输入,一个输出
package demo; public class resource { public string name; public string sex; }
输入线程:
package demo; public class input implements runnable { private resource r = new resource(); public void run() { int i = 0; while (true) { if (i % 2 == 0) { r.name = "张三"; r.sex = "男"; } else { r.name = "李四"; r.sex = "女"; } i++; } } }
输出线程:
package demo; public class output implements runnable { private resource r = new resource(); public void run(){ while (true) { system.out.println(r.name+"..."+r.sex); } } }
测试类:
package demo; public class threaddemo { public static void main(string[] args) { input in = new input(); output out = new output(); thread tin = new thread(in); thread tout = new thread(out); tin.start(); tout.start(); } }
运行后却发现输出的都是null...null
因为输入线程和输出线程中创建的resource对象使不同的
解决null问题:
package demo; public class input implements runnable { private resource r; public input(resource r){ this.r = r; } public void run() { int i = 0; while (true) { if (i % 2 == 0) { r.name = "张三"; r.sex = "男"; } else { r.name = "李四"; r.sex = "女"; } i++; } } }
package demo; public class output implements runnable { private resource r; public output(resource r){ this.r = r; } public void run(){ while (true) { system.out.println(r.name+"..."+r.sex); } } }
package demo; public class threaddemo { public static void main(string[] args) { resource r = new resource(); input in = new input(r); output out = new output(r); thread tin = new thread(in); thread tout = new thread(out); tin.start(); tout.start(); } }
运行后又发现了另一个问题:
输出中含有:张三...女或者李四...男,性别出错
发生原因:
赋值完张三和男后,继续赋值李四和女,这时候还未还得及赋值女,就进入了输出线程,这时候就会输出李四...男
于是想到加上同步:
public void run() { int i = 0; while (true) { synchronized (this) { if (i % 2 == 0) { r.name = "张三"; r.sex = "男"; } else { r.name = "李四"; r.sex = "女"; } i++; } } }
public void run() { while (true) { synchronized (this) { system.out.println(r.name + "..." + r.sex); } } }
然而问题并没有解决:
原因:
这里的同步失去了作用,用到的不是一个锁
解决办法:
使用一个共同的锁即可
public void run() { int i = 0; while (true) { synchronized (r) { if (i % 2 == 0) { r.name = "张三"; r.sex = "男"; } else { r.name = "李四"; r.sex = "女"; } i++; } } }
public void run() { while (true) { synchronized (r) { system.out.println(r.name + "..." + r.sex); } } }
这时候就是正常的输出了
但是还是存在一个问题,我们希望的是张三和李四交错出现,一个张三一个李四,现在依然是随机出现的,大片的张三或李四
解决办法:
先让input线程赋值,然后让output线程输出,并且让输入线程等待,不允许再赋值李四,等待输出张三结束后,再允许李四赋值,依次下去
输入线程也需要同样的方式,输出完后要等待
这时候就需要用到等待唤醒机制:
输入:赋值后,执行方法wait()永远等待
输出:打印后,再输出等待之前,唤醒输入notify(),自己再wait()永远等待
输入:被唤醒后,重新赋值,必须notify()唤醒输出的线程,自己再wait()等待
依次循环下去
代码实现:
package demo; public class resource { public string name; public string sex; public boolean flag = false; }
package demo; public class input implements runnable { private resource r; public input(resource r) { this.r = r; } public void run() { int i = 0; while (true) { synchronized (r) { if (r.flag) { try { r.wait(); } catch (exception e) { } } if (i % 2 == 0) { r.name = "张三"; r.sex = "男"; } else { r.name = "李四"; r.sex = "女"; } r.flag = true; r.notify(); } i++; } } }
package demo; public class output implements runnable { private resource r; public output(resource r) { this.r = r; } public void run() { while (true) { synchronized (r) { if (!r.flag) { try { r.wait(); } catch (exception e) { } } system.out.println(r.name + "..." + r.sex); r.flag = false; r.notify(); } } } }
package demo; public class threaddemo { public static void main(string[] args) { resource r = new resource(); input in = new input(r); output out = new output(r); thread tin = new thread(in); thread tout = new thread(out); tin.start(); tout.start(); } }
这时候就是张三李四交错输出了
完成