多线程学习
1.概念
程序·进程·线程
进程是指执行一个程序的执行过程,一个进程会有多个线程。
即一个任务就可以理解为一个线程,线程是CPU调试和执行的单位。
2.前期准备
配置commons io工具包,里面提供了大量与文件相关的类
-
百度搜索commons io,选择下载最新版本的
-
在项目src目录下创建lib包,把下载好的文件放进去
-
然后右键lib包,点击add Build Path就可以使用了
3.线程创建
-
方法一:继承Thread类
-
子类继承Thread类具备多线程能力
-
启动线程:子类对象.start()
-
不建议使用,避免OOP单继承局限性
//创建线程方法一:继承Thread类,重写run方法,调用start方法开启线程 //总结:线程开始不一定立即执行,由CPU调度执行 public class TestThread01 extends Thread { @Override public void run() { //run方法线程体 for (int i = 0; i < 20; i++) { System.out.println("我在学习代码--" + i); } } public static void main(String[] args) { //main方法线程,主线程体 TestThread01 textThread01 = new TestThread01(); //调用start方法开始线程 textThread01.start(); for (int i = 0; i < 20; i++) { System.out.println("我在学习多线程--" + i); } } }
-
-
方法二:实现Runnable接口
-
实现Runnable具备多线程能力
-
启动线程:传入目标对象+Thread对象.start()
-
推荐使用,避免单继承局限性,灵活方便,方便同一个对象被多个线程使用
//创建线程方法2:实现runnable接口,重新run方法,执行线程需要丢人runnable接口实现类,调用start方法启动. public class TestThread03 implements Runnable{ @Override public void run() { //run方法线程体 for (int i = 0; i < 100; i++) { System.out.println("我在学习代码--" + i); } } public static void main(String[] args) { //创建runnable接口实现类对象 TestThread03 testThread03 = new TestThread03(); //创建线程对象,通过线程对象来开启我们的线程,代理 Thread thread = new Thread(testThread03); thread.start(); for (int i = 0; i < 1000; i++) { System.out.println("我在学习多线程--" + i); } } }
-
!多个线程操作同一个资源的情况下,线程不安完全,出现紊乱。
-
实现Callable接口,需要返回值类型
-
重写call方法,需要抛出异常
-
创建目标对象
-
创建执行服务:ExecutorService ser =Executors.newFixedThreadPool(1);
-
提交执行:Future result1 = ser.submit(t1);
-
获取结果:boolean r1 = result1.get()
-
关闭服务:ser.shutdownNow();
//创建执行服务 ExecutorService ser = Executors.newFixedThreadPool(3); //提交执行 Future<Boolean> r1 = ser.submit(t1); Future<Boolean> r2 = ser.submit(t2); Future<Boolean> r3 = ser.submit(t3); //获取结果 boolean rs1 = r1.get(); boolean rs2 = r2.get(); boolean rs3 = r3.get(); //关闭服务 ser.shutdownNow(); //打印 System.out.println(rs1); System.out.println(rs2); System.out.println(rs3);
4.线程状态
-
线程停止
1.建议线程正常停止-->利用次数,不建议死循环 public static void main(String[] args) { TestStop testStop = new TestStop(); new Thread(testStop).start(); for (int i = 0; i < 1000; i++) { System.out.println("main"+i); if (i==900){ //调用stop方法切换标志位,让线程停止 testStop.stop(); System.out.println("线程该停止了"); } } } 2.建议使用标志位-->设置一个标志位 //设置一个标志位 private boolean flag = true; @Override public void run() { int i = 0; while (flag){ System.out.println("run...Thread"+i); } } //设置一个公开的方法停止线程,转换标志位 public void stop(){ this.flag = false; } 3.不要使用stop、destroy等过时的方法或者JDK不建议使用的方法
-
线程休眠_sleep
-
模拟网络延时
//模拟延时 try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); }
-
模拟倒计时
//10秒倒计时 try { tenDown(); } catch (InterruptedException e) { e.printStackTrace(); } //模拟倒计时,, public static void tenDown() throws InterruptedException { int num = 10; do { Thread.sleep(1000); System.out.println(num--); } while (num > 0); } }
-
-
线程礼让_yield
//测试线程礼让 //线程礼让,是让当前正在执行的线程停止,但不阻塞 //礼让不一定成功,看CPU心情 public class TestYield { public static void main(String[] args) { MyYield myYield = new MyYield(); new Thread(myYield,"a").start(); new Thread(myYield,"b").start(); } } class MyYield implements Runnable{ @Override public void run() { System.out.println(Thread.currentThread().getName()+"线程开始执行"); Thread.yield();//礼让 System.out.println(Thread.currentThread().getName()+"线程停止执行"); } }
-
线程强制执行_join
//测试join方法 //想象为插队 public class TestJoin implements Runnable{ @Override public void run() { for (int i = 0; i < 200; i++) { System.out.println("线程vip来了..."+i); } } public static void main(String[] args) throws InterruptedException { TestJoin testJoin = new TestJoin(); Thread thread = new Thread(testJoin); thread.start(); //主线程 for (int i = 0; i < 500; i++) { if (i == 100){ thread.join();//插队 } System.out.println("线程main"+i); } } }
-
线程的优先级
- 优先级低只是意味着获得调度的概率低,并不是优先级低就不被调用,还嘚看CPU的调度
- 使用getPriority()方法获取优先值,setPriority()方法设置优先值
-
守护线程
//测试守护线程
/*
1.线程分为用户线程和守护线程
2.虚拟机必须确保用户线程执行完毕
3.虚拟机不用等待守护线程执行完毕
*/
public class TestDaemon {
public static void main(String[] args) {
God god = new God();
Thread thread = new Thread(god);
thread.setDaemon(true);//默认是false表示用户线程,正常的线程都是用户线程
thread.start();
5.线程同步
-
同步块:synchronized(Obj){ },Obj称之为同步监视器
-
Obj可以是任何对象,但是推荐使用共享资源作为同步监视器
-
同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身
//synchronized 同步方法,锁的是this private synchronized void buy() throws InterruptedException { if (ticketNums<=0){ flag = false; return; } //模拟延时 Thread.sleep(100); //买票 System.out.println(Thread.currentThread().getName()+"拿到了"+ticketNums--); }
-
-
同步监视器的执行过程
- 第一个线程访问,锁定同步监视器,执行其中代码
- 第二个线程访问,发现同步监视器被锁定,无法访问
- 第一个线程访问完毕,解锁同步监视器
- 第二个线程访问,发现同步监视器没有锁,然后锁定并访问
//synchronized 锁的对象应该是变化的量,需要增删改的量 synchronized (account){//判断有没有钱 if (account.money-drawingMoney<0){ System.out.println(Thread.currentThread().getName()+"钱不够了,取不了"); return; } //模拟延时 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //卡内余额=余额-你取的钱 account.money = account.money-drawingMoney; //你现在手里的钱=手头的钱+你取的钱 nowMoney = nowMoney+drawingMoney; //打印 System.out.println(account.name+"余额为:"+account.money); System.out.println(this.getName()+"手里的钱:"+nowMoney);//这里this.getName() = Thread.currentThread().getName() }
6.死锁
- 必要条件
(1)互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求该资源,则请求者只能等待,直至占有该资源的进程用毕释放。
(2)请求和保持条件:指进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源又被其它进程占有,此时请求进程阻塞,但又对自己获得的其它资源保持不放。
(3)不剥夺条件:指进程已获得资源,在使用完之前,不能被剥夺,只能在使用完时由自己释放。
(4)环路等待条件:指在发生死锁时,必然存在一个进程—资源的环形链,即进程集合(P0,P1,P2,…,Pn)中的P0正在等待一个P1占用的资源;P1正在等待一个P2占用的资源,……,Pn正在等待已被P0占用的资源。
-
死锁例子及解决办法
//多个线程互相抱着对方需要的资源,然后形成僵持 //化妆:互相持有对方的锁,就是需要拿到对方的钱 private void makeup() throws InterruptedException { if (choice==0){ synchronized (lipstick){ System.out.println(this.girlName+"获得口红的锁"); Thread.sleep(1000); synchronized (mirror){ System.out.println(this.girlName+"获得镜子的锁"); } } //改为下面情况就可以破解死锁,放在外面,不能同时拥有两个锁 /*synchronized (mirror){ System.out.println(this.girlName+"获得镜子的锁"); }*/ }else { synchronized (mirror){ System.out.println(this.girlName+"获得镜子的锁"); Thread.sleep(2000); synchronized (lipstick){ System.out.println(this.girlName+"获得口红的锁"); } } //改为下面情况就可以破解死锁,放在外面,不能同时拥有两个锁 /*synchronized (lipstick){ System.out.println(this.girlName+"获得口红的锁"); }*/ } }
-
Lock锁
1)Lock锁是个Java类,用ReentrantLock()方法定义,用法如下
//定义lock锁 private final ReentrantLock lock = new ReentrantLock(); try { lock.lock();//加锁 while (ticketNums>0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(ticketNums--); } }finally { lock.unlock();//解锁 }
2)lock锁和synchronized区别
- 首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
- synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁
- synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
- 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
- synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)
- Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
7.线程协作
-
生产者—消费者模型1–>管程法,借助缓冲区
理解:
生产者将生产好的数据放入缓冲区 , 消费者从缓冲区拿出数据。通过判断缓冲区大小来决定生产者何时生产,消费者何时消费。
模型:
- 生产者 : 负责生产数据的模块 (可能是方法 , 对象 , 线程 , 进程) ;
- 消费者 : 负责处理数据的模块 (可能是方法 , 对象 , 线程 , 进程) ;
- 缓冲区 : 消费者不能直接使用生产者的数据 , 而是通过缓冲区拿出数据。
生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据
//测试:生产者消费者模型-->利用缓冲区解决:管程法
//需要对象:生产者,消费者,缓冲区,产品
public class TestPC {
public static void main(String[] args) {
SynContainer synContainer = new SynContainer();
new Productor(synContainer).start();
new Consumer(synContainer).start();
}
}
//生产者
class Productor extends Thread{
SynContainer container;
public Productor(SynContainer container){
this.container = container;
}
//生产
@Override
public void run(){
for (int i = 0; i < 100; i++) {
container.push(new Hamburger(i));
System.out.println("生产了"+i+"只鸡");
}
}
}
//消费者
class Consumer extends Thread{
SynContainer container;
public Consumer(SynContainer container){
this.container = container;
}
//消费
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("消费了-->"+container.pop().id+"只鸡");
}
}
}
//产品
class Hamburger{
int id;//产品编号
public Hamburger(int id){
this.id = id;
}
}
//缓冲区
class SynContainer{
//容器大小
Hamburger[] hamburgers = new Hamburger[10];
//计数器
int counter;
//生产者放入产品,此处嘚用上同步
public synchronized void push(Hamburger hamburger){
//如果容器满了,则等待消费
if (counter == hamburgers.length){
//通知消费者消费
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果没有满,则生产产品
hamburgers[counter] = hamburger;
counter++;
//可以通知消费者消费了
this.notifyAll();
}
//消费者取出产品,此处嘚用上同步
public synchronized Hamburger pop (){
//判断能否消费
if (counter==0){
//等待生产者生产
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果可以消费了
counter--;
Hamburger hamburger = hamburgers[counter];
//通知生产者生产
this.notifyAll();
return hamburger;
}
}
-
生产者—消费者模型2–>信号灯法,借助标志位
理解:
当标志位为真时,生产者生产,接着把标志位变为假,等待消费者消费,消费完后接着把标志位变为真,如此一直循环下去。
模型:
- 生产者:负责生产数据的模块(这里的模块可能是:方法、对象、线程、进程)
- 消费者:负责处理数据的模块(这里的模块可能是:方法、对象、线程、进程)
- 标志位:生产者消费者使用同一资源,他们之间有个标志位,类似于信号灯的作用,通过信号灯控制生产者和消费者的循环使用
//测试:生产者消费者模型-->利用标志位解决:信号灯法
public class TestPC2 {
public static void main(String[] args) {
TV tv = new TV();
new Player(tv).start();
new Watch(tv).start();
}
}
//生产者-->演员
class Player extends Thread{
TV tv;
public Player(TV tv){
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 50; i++) {
if (i%2==0){
this.tv.play("小品节目表演中");
}else {
this.tv.play("歌唱大赛进行中");
}
}
}
}
//消费者-->观众
class Watch extends Thread{
TV tv;
public Watch(TV tv){
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 50; i++) {
this.tv.watch();
}
}
}
//产品-->节目
class TV{
//表演的节目
String program;
//设置标志位
boolean flag = true;//T,演员表演,观众等待;F,观众观看,演员等待
//表演
public synchronized void play(String program){
//如果标志位为F,演员等待
if (!flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("演员表演了-->"+program);
//通知观众观看
this.notifyAll();
this.program = program;
this.flag = !flag;
}
//观看
public synchronized void watch(){
//如果标志位为T,观众等待
if (flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("观众观看了:"+program);
//通知演员表演
this.notifyAll();
this.flag = !flag;
}
}