一、Java 中的线程
二、Java 中开启子线程的方式
1、继承Thread类
将线程类转换为线程对象
1、线程对象就要开始争抢cpu资源了;
2、当前线程类必须继承Thread类并且重写run方法;
3、具体实现的代码要写在run方法里面;
4、想要执行线程对象里面的run方法,就需要先创建这个类的对象并且调用start方法;
买火车票案例
public class BuyTicketThread extends Thread{
private static int ticketNum = 10;
public BuyTicketThread(String name) {
super(name);
}
@Override
public void run() {
//每个窗口有100个人抢票
//每个线程抢票100次
for (int i = 0; i < 100; i++) {
if (ticketNum > 0)
System.out.println(this.getName() + "买到车票============" + ticketNum--);
}
}
}
public class BuyTicketTest {
public static void main(String[] args) {
BuyTicketThread thread1 = new BuyTicketThread("线程1");
thread1.start();
BuyTicketThread thread2 = new BuyTicketThread("线程2");
thread2.start();
BuyTicketThread thread3 = new BuyTicketThread("线程3");
thread3.start();
}
}
2、实现Runnable接口
创建线程类实现Runnable接口,并且实现run方法
public class Thread02 implements Runnable {
@Override
public void run() {
//输出1-10
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "============" + i);
}
}
}
// 创建子线程对象 注意这时还不能直接用
Thread02 thread02 = new Thread02();
// 构建子线程 将子线程对象传入Thread类的构造器中,这时的对象才是真正的子线程对象
Thread thread = new Thread(thread02);
// 运行子线程的run方法
thread.start();
// 调用start方法,实际就是调用Thread构造器入参对象的run方法。
买火车票实例
public class BuyTicketThread implements Runnable{
private int ticketNum = 10;
@Override public void run() {
//每个窗口有100个人抢票
//每个线程抢票100次
for (int i = 0; i < 100; i++) {
if (ticketNum > 0)
System.out.println(Thread.currentThread().getName() + "买到车票============" + ticketNum--);
}
}
}
public class BuyTicketTest {
public static void main(String[] args) {
BuyTicketThread thread = new BuyTicketThread();
Thread t1 = new Thread(thread, "线程1");
t1.start();
Thread t2 = new Thread(thread, "线程2");
t2.start();
Thread t3 = new Thread(thread, "线程3");
t3.start();
}
}
小结
1、使用实现Runable接口的方式创建线程好于继承Thread类,java单继承的特点。
2、Thread类和Runnable接口具有以下关系:
3、实现的run方法具有以下不足:
-- 不能有返回值。
-- 无法抛出异常。
3、实现Callable接口
1、好处:有返回值,可以抛出异常
2、缺点:线程的创建比较麻烦
3、实现Callable接口可以指定泛型,不指定泛型默认返回值类型为Object,否则为指定的类
// 创建子线程类,实现Callable接口,实现call方法
public class TestRandom implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return new Random().nextInt(10);
}
}
// 创建子线程对象,但是这时这个对象还不能直接使用
TestRandom testRandom = new TestRandom();
// 创建一个中间对象,传入上一步的子线程对象,这一步是为了兼容Thread类的构造器的参数
utureTask futureTask = new FutureTask(testRandom);
// 将上一步的中间对象传入Thread类的构造器,的带真正的子线程对象
Thread thread = new Thread(futureTask);
thread.start();
// 获取子线程功能的返回值,需要借助第6步的中间对象的get()来获取到返回值
Object object = futureTask.get();
三、线程的生命周期
从开始到消亡,线程经历哪些阶段:
四、线程常见方法
1、start():
-- 作用:启动当前线程;
-- 用法:thread.start();线程对象直接调用;
-- 底层逻辑:表面上调用start方法,实际上调用线程里面的run方法;
2、run():
-- 作用:run方法里面写的是具体的业务逻辑;
-- 用法:子线程类 继承Thread类或者实现Runnable接口的时候,重写的这个run方法;
-- 底层逻辑:调用start方法后,cpu给线程分配资源后运行的就是这个run方法;
3、Thread.currentThread():
-- 作用:获取当前正在运行的线程对象,Thread类中的一个静态方法;
-- 用法:因为是静态方法,所以写法为 Thread.currentThread();
4、setName(),getName():设置、读取线程名字;
5、setPriority(Integer integer):
-- 作用:设置优先级;
-- 用法:thread对象.setPriority(Integer integer);
-- 底层逻辑:
-- 同优先级别的线程,采用先到先服务的策略,使用时间片策略;
-- 如果优先级别高,那么被CPU调度的概率高;
-- 不是级别越高一定会先执行;
-- 级别分为 1 ~ 10分,默认为5分;
6、join():
-- 作用:使线程被先执行,而且会将它完全执行完后,才会执行其他线程;
-- 用法:必须先start,再join才会生效;
-- 底层逻辑:相当于调度这个线程的时候,给他的时间是整块的,不是分片的;
-- 测试代码
public class TestThread extends Thread{ public TestThread(String name) { super(name); } @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(this.getName() + "====" + i); } } } class Test{ public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 100; i++) { if (i == 6){ //创建子线程 TestThread testThread = new TestThread("子线程"); testThread.start(); testThread.join(); } System.out.println("main=====" + i); } } }
7、Thread.sleep():人为地制造阻塞;
-- 作用:使线程睡眠 N 毫秒;
-- 用法:thread对象.sleep(1000);
-- 底层逻辑:
1、主动让出当前CPU
2、在N毫秒内不参与竞争
3、时间过去重新参与竞争;
4、开始参与竞争也不会立刻被执行;
8、Thread.setDaemon():
-- 伴随线程:皇上 --> 驾崩 --> 妃子陪葬;妃子死前垂死挣扎;
-- 作用:设置 子线程 为 主线程 的伴随线程;将子线程设置为主线程的 妃子;
-- 用法:先setDaemon(true),再start();
-- 也叫守护线程;
9、Thread.currentThread.stop():
-- 一个过期的方法,不推荐使用;
-- 作用:立即弄死当前线程;
-- 用法:thread.stop();
五、同步代码块
1、锁 --> 加同步 --> 同步监视器
2、同步代码块1
使用 synchronized关键字给 this 上锁,将共享资源锁住;
this 其实就是调用run方法的对象,此时这个对象在内存中是唯一的;
synchronized关键字应该只限制有安全隐患的代码,以提高效率;
3、同步代码块2
4、同步监视器
写法:synchronized(同步监视器){};
1、要求synchronized锁住的资源在内存中是唯一的,这个同步监视器用来给线程标识目标代码块是否可以执行;
2、不能是基本数据类型,只能是引用数据类型;
3、不推荐使用 String和Integer包装类对象当作 锁子;
4、可以创建一个唯一的无意义对象当 锁子;
5、推荐使用 final 修饰 锁子;
6、一般使用共享资源做 锁子;
7、锁子的指向地址不能修改,锁子内部的属性不能修改;
5、在同步代码块中线程切换过程
1、线程【1】来了,发现锁子状态是 open;
2、线程【1】将锁子状态改成 close,线程【1】进入同步代码块;
3、线程【2】来了,发现锁子状态是 close,那么不进入同步代码块;
4、线程【1】执行完成,将锁子状态改成 open;
5、线程【2】检查锁子状态后进入同步代码块;
注意:锁子上锁时,会发生CPU资源切换,但是没锁的线程执行不了同步代码块;
问题:同步代码块可以发生CPU资源的切换吗? -- 能;
但是没拿到锁的线程无法执行同步代码块;
多个同步代码块使用了同一个 锁子(同步监视器):
假如 锁子 A 被上锁,那么其他所有使用 A 的同步代码块都被锁住了;
假如 锁子 A 被上锁,锁子 B 没被上锁,那么使用锁子 B 的同步代码块可以被其他线程运行;
六、同步方法
多个线程对象调用,同步方法用static修饰;相当于锁上了 BuyTicketThread.class;
单个线程对象调用;相当于锁上了 this;
锁必须是共享锁,且锁在内存中是唯一的;
不建议将 run() 方法定义为同步方法;
同步代码块效率高于同步方法;
同步方法锁的是 this,所以一旦锁住了一个方法,那么其他被synchronized修饰的方法也被锁住了;
七、Lock 锁
使用多态创建锁对象,灵活
Lock 与 synchronized 的区别
Lock 是显式锁,需要手动开/关锁;synchronized 是隐式锁;
Lock 只有代码块锁;synchronized 有代码块锁和方法锁;
Lock锁效率高于 synchronized;
八、使用 同步/锁 特点
优点 | 缺点 |
数据可以保持同步; | 效率低; |
可能造成死锁; -- 死锁:你需要我的,我需要你的;一直僵持; 解决死锁办法: -- 减少同步资源的定义; -- 避免嵌套 同步/锁子; |
九、线程通信
1、使用同步代码块实现资源同步。
2、VO 类使用同步方法实现资源同步。
3、线程通信测试代码
public class Product {
private String brand;
private String name;
//引入一个灯 true 红色 false - 绿色
//默认没有商品 让生产者先生产
boolean flag = false;
//生产商品
public synchronized void setProduct(String brand,String name){
//如果是红色,那么不生产,就等待
if (flag){
try {
wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//生产
this.setBrand(brand);
try {
Thread.sleep(100);
}catch (Exception e){
e.printStackTrace();
}
this.setName(name);
System.out.println("生产者生产了" + this.getBrand() + "-----" + this.getName());
//生产完成后,灯变成红色
flag = true;
//通知消费者
notify();
}
//消费商品
public synchronized void getProduct(){
//没有商品就等待
if (!flag){
try {
wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//有商品就消费
System.out.println("消费者消费了=====" + this.getBrand() + this.getName());
//灯变色 变成绿色
flag = false;
//通知生产者
notify();
}
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
4、锁池 和 等待池
在 java 锁对象中,有两种池,一种是锁池,一种是等待池;
锁池 | synchronized |
等待池 | wait(),notify(),notifyAll() |
如果一个线程调用了某个对象的wait()方法,那么该线程进入到这个对象的等待池中,并且放弃锁;
如果未来的另一个线程调用了相同对象的notify()、notifyAll()方法,等待池中的线程就会被唤醒,可以重新进入锁池争抢该对象的锁;
如果被唤醒的线程抢到锁了,那么它会沿着之前调用wait()方法之后的代码继续执行;注意是沿着wait方法之后;
注意:
①、wait()和notify()方法,必须放在同步代码块/同步方法中才能生效(因为在同步的基础上进行线程的通信才是有效的)
②、sleep()和wait()方法的区别是:sleep()不会放弃对锁的占有权,wait()会放弃对锁的占有权;
5、使用Lock类来增强线程通信
Condition 类来增强等待池,可以将原先的一个等待池拆分为多个等待池;
Condition condition = lock.newCondition();
condition.await() == wait();
condition.signal() == notify();
condition.signalAll() == notifyAll();
Lock lock = new ReentrantLock();
//定义 等待池
Condition produceCondition = lock.newCondition();
Condition consumerCondition = lock.newCondition();
public void setProduct(String brand,String name){
lock.lock();
try {
if (flag){ //如果是红色,那么不生产,就等待
try {
// wait();
// 生产者阻塞
// 生产者进入等待队列
produceCondition.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//生产
this.setBrand(brand);
try {
Thread.sleep(100);
}catch (Exception e){
e.printStackTrace();
}
this.setName(name);
System.out.println("生产者生产了" + this.getBrand() + "-----" + this.getName());
//生产完成后,灯变成红色
flag = true;
// notify(); //通知消费者
// 唤醒消费者
// 消费者等待池
consumerCondition.signal();
}catch (Exception e){
}finally {
lock.unlock();
}
}