Day24 多线程
1. 多线程
1.7 常用方法
- start():启动线程的唯一方式
- setName():设置线程的名字 默认是Thread-0,Thread-1
- getName():获取线程的名字
- setPriority():设置线程优先级
- getPriority():获取线程优先级
- static currentThread():获取当前线程的内存地址
- static sleep():睡眠当前线程,参数是睡眠的毫秒数
- 静态的,和用哪个对象没关系,写在哪个线程中,就获取哪个线程对象,就睡眠哪个线程
1.8 生命周期
JDK中用Thread.State类定义了线程的几种状态
要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类 及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五 种状态:
新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建 状态
就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线 程的操作和功能
阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态
死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
1.9 终止线程
public static void main(String[] args) {
Processer_02 p = new Processer_02();
Thread t1 = new Thread(p);
t1.setName("t1");
t1.start();
// t1线程是死循环,5秒后,终止
try {
Thread.sleep(5000);// 睡眠的main方法,5秒后执行stop
} catch (Exception e) {
e.printStackTrace();
}
// t1.stop();
// 推荐使用占位符的方式去终止
p.flag = false;
}
}
class Processer_02 implements Runnable {
boolean flag = true;
public void run() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM--dd HH:mm:ss SSS");
while (flag) {
System.out.println(Thread.currentThread().getName() + ":" + sdf.format(new Date()));
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
1.10 线程控制
* join 合并线程,多个线程合并为一个线程
* public static void main(String[] args) {
Thread t1 = new Processer_03();
t1.setName("t1");
t1.start();
// 多线程同时执行
// 合并 让当前线程等待指定线程执行完后再执行
try {
t1.join();
} catch (Exception e) {
e.printStackTrace();
}
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
class Processer_03 extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
* yield():暂停当前线程,让位给其他等待中的线程执行
*
* 是个静态方法,使用Thread.yield() 进行调用
*
* 同优先级让位,不同优先级不让位
* public static void main(String[] args) {
Thread t1 = new Thread(new Processor_04());
t1.start();
for (int i = 0; i < 10; i++) {
// 让出当前执行的时间片,让等待中的线程先执行
Thread.yield();
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
class Processor_04 implements Runnable {
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
1.11 线程同步
1.11.1 概述
当多个线程有可能操作同一个数据的时候,为了考虑数据的一致性和安全性,需要进行同步机制,尤其是更改操作,查询无所谓,因为查询不会对数据发生更改
1 同步代码块
Synchronized(对象){
// 需要同步的代码
}
2 还可以锁方法,使用synchronized 修饰方法即可
1.11.2 多线程可能出现的问题
因为多线程并行问题,导致多个线程操作了同一个数据,导致结果不一致
1.11.3 如何解决
public static void main(String[] args) {
Account account = new Account(3000);
Thread t1 = new Thread(new Processor_06(account));
Thread t2 = new Thread(new Processor_06(account));
t1.setName("t1");
t2.setName("t2");
// 先执行不代表先执行完
// 在前面只能说先启动的概率大,不是一定先执行
// 多线程不是很绝对
t2.start();
t1.start();
}
}
class Processor_06 implements Runnable {
Account account;
public Processor_06(Account account) {
this.account = account;
}
public void run() {
account.withDrow(1000);
}
}
class Account {
private double balance;
public Account(double balance) {
super();
this.balance = balance;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
// synchronized:修饰符,修饰的方法不能被多个线程同时执行
// public synchronized void withDrow(double money) {
// System.out.println("不需要同步的其他功能");
// try {
// Thread.sleep(5000);
// } catch (Exception e) {
// e.printStackTrace();
// }
// double after = balance - money;
// balance = after;
// System.out.println(Thread.currentThread().getName() +
// "-->取款成功:1000.0,余额为:" + balance);
// }
public void withDrow(double money) {
System.out.println("不需要同步的其他功能");
// 下面代码 需要同步执行
synchronized (this) {
// try {
// Thread.sleep(5000);
// } catch (Exception e) {
// e.printStackTrace();
// }
double after = balance - money;
balance = after;
System.out.println(Thread.currentThread().getName() + "-->取款成功:1000.0,余额为:" + balance);
}
}
1.12 Lock
1.12.1 概述
1 Lock 是显式锁,需要手动开启和手动关闭,而synchronized是隐式锁,除了作用域,自动解锁
2 Lock 只有代码块锁,而synchronized可以代码块锁,可以方法锁
3 使用Lock还有更多功能扩展
1.12.2 使用
public static void main(String[] args) {
Account1 account = new Account1(3000);
Thread t1 = new Thread(new Processor_07(account));
Thread t2 = new Thread(new Processor_07(account));
Thread t3 = new Thread(new Processor_07(account));
t1.setName("t1");
t2.setName("t2");
t3.setName("t3");
t1.start();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
t3.start();
}
1.13 死锁
1.13.1 概述
1.13.2 原理
- 死锁:就是大家在执行过程中,都遇到了对方进入加锁的方法,从而导致了都访问不了的状态
- 1 某个线程执行完成,需要 先后 嵌套 锁定两个对象,在这个过程中,该线程先锁定了第一个对象
- 2 另一个线程执行完成,需要 先后 嵌套 锁定两个对象, 在这个过程中,该线程先锁定了第二个对象
- 3 第一个线程执行中,执行到第二个对象的时候,发现第二个对象被第二个线程锁住了,只能等待
- 4 第二个线程执行,执行到锁一个对象的时候,发现第一个对象被第一个线程锁住了,只能等待
1.13.3 编码
public class Thread_08_DeadLock {
public static void main(String[] args) {
for (int i = 0; i < 1025; i++) {
Object o1 = new Object();
Object o2 = new Object();
Thread t1 = new T1(o1, o2);
Thread t2 = new Thread(new T2(o1, o2));
t1.start();
t2.start();
System.out.println(i);
}
}
}
class T1 extends Thread {
Object o1;
Object o2;
public T1(Object o1, Object o2) {
super();
this.o1 = o1;
this.o2 = o2;
}
@Override
public void run() {
// 当我们使用代码块锁,锁住o1的时候,那么o1对象的所有代码块锁和所有加锁的成员方法,全部锁定
synchronized (o1) {
// 加睡眠 是为了 让t2线程先锁住o2
try {
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println("T2已经对o2加锁");
}
}
System.out.println("T1执行完成,o1和o2解锁");
}
}
class T2 implements Runnable {
Object o1;
Object o2;
public T2(Object o1, Object o2) {
super();
this.o1 = o1;
this.o2 = o2;
}
@Override
public void run() {
synchronized (o2) {
try {
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (o1) {
System.out.println("T2已经对o1加锁");
}
}
System.out.println("T2执行完成,o1和o2解锁");
}
}
1.14 线程通信
1.14.1 概述
1.14.2 生产者和消费者
★简介
生产者消费者模式并不是GOF提出的23种设计模式之一,23种设计模式都是建立在面向对象的基础之上的,但其实面向过程的编程中也有很多高效的编程模式,生产者消费者模式便是其中之一,它是我们编程过程中最常用的一种设计模式。
在实际的软件开发过程中,经常会碰到如下场景:某个模块负责产生数据,这些数据由另一个模块来负责处理(此处的模块是广义的,可以是类、函数、线程、进程等)。产生数据的模块,就形象地称为生产者;而处理数据的模块,就称为消费者。
单单抽象出生产者和消费者,还够不上是生产者/消费者模式。该模式还需要有一个缓冲区处于生产者和消费者之间,作为一个中介。生产者把数据放入缓冲区,而消费者从缓冲区取出数据。大概的结构如下图。
为了不至于太抽象,我们举一个寄信的例子(虽说这年头寄信已经不时兴,但这个例子还是比较贴切的)。假设你要寄一封平信,大致过程如下:
1、你把信写好——相当于生产者制造数据
2、你把信放入邮筒——相当于生产者把数据放入缓冲区
3、邮递员把信从邮筒取出——相当于消费者把数据取出缓冲区
4、邮递员把信拿去邮局做相应的处理——相当于消费者处理数据
★优点
可能有同学会问了:这个缓冲区有什么用捏?为什么不让生产者直接调用消费者的某个函数,直接把数据传递过去?搞出这么一个缓冲区作甚?
其实这里面是大有讲究的,大概有如下一些好处。
◇解耦
假设生产者和消费者分别是两个类。如果让生产者直接调用消费者的某个方法,那么生产者对于消费者就会产生依赖(也就是耦合)。将来如果消费者的代码发生变化,可能会影响到生产者。而如果两者都依赖于某个缓冲区,两者之间不直接依赖,耦合也就相应降低了。
接着上述的例子,如果不使用邮筒(也就是缓冲区),你必须得把信直接交给邮递员。有同学会说,直接给邮递员不是挺简单的嘛?其实不简单,你必须得认识谁是邮递员,才能把信给他(光凭身上穿的制服,万一有人假冒,就惨了)。这就产生和你和邮递员之间的依赖(相当于生产者和消费者的强耦合)。万一哪天邮递员换人了,你还要重新认识一下(相当于消费者变化导致修改生产者代码)。而邮筒相对来说比较固定,你依赖它的成本就比较低(相当于和缓冲区之间的弱耦合)。
或者比如我们现在取快递或者取外面,一般我们上课或者上班 ,不方便拿快递,所以 只需要把快递放到相应的投放点即可,比如菜鸟驿站,此时 菜鸟驿站就是这个缓冲区,而我们和快递员之间的耦合度就降低了,相互之间也不用认识
1.15 单例模式
1.15.1 概述
让某个类只实例化一次对象
1 构造方法私有化
2 静态变量保存创建的对象
3 公共的静态方法,用于获取当前类对象
1.15.2 之前编码
public class Singleton_02 {
private Singleton_02() {
}
private static Singleton_02 s;
public static Singleton_02 getInstance() {
if (s == null) {
s = new Singleton_02();
}
return s;
}
}
1.15.3 问题和解决
可以使用synchronized解决
可以解决,但是效率低,因为我们只需要在第一次创建对象的时候,排序执行即可,
一旦对象创建完成,后续就算有多个线程同时进入判断,都为false
所以此时我们应该优先考虑代码块锁而不是方法锁
public class SingLeton_01 {
private SingLeton_01() {
}
// 防止指令重排
private volatile static SingLeton_01 s = null;
public static SingLeton_01 getInstance() {
if (s == null) {
// 类锁,类中所有加锁的静态方法和语句块锁 全部锁定
synchronized (SingLeton_01.class) {
// 双重校验 防止创建多个对象
if (s == null) {
s = new SingLeton_01();
}
}
}
return s;
}
}