Java 多线程
进程
程序是静止的,只有真正运行时的程序,才能成为进程
特点:
- 单核CPU在任何时间点上只能运行一个进程。
- 宏观并行、微观串行
线程
线程:又称轻量级进程(Light Weight Process)
- 程序中的一个顺序控制流程,同时也是CPU的基本调度单位
- 进程由多个线程组成,彼此间完成不同的工作,交替执行,称为多线程
进程和线程的区别
- 进程是操作系统资源分配的基本单位,而线程是CPU的基本调度单位
- 一个程序运行后至少有一个进程
- 一个进程可以包含多个线程,但至少需要一个线程
- 进程间不能共享数据段地址,但同进程的线程间可以
线程组成
任何线程都具有的基本的组成部分
- CPU(时间片):操作系统(OS)会为每个线程分配时执行时间
- 运行数据
- 堆空间:存储线程需使用的对象,多个线程可以共享堆中的对象
- 栈空间:存储线程需使用的局部变量,每个线程都拥有独立的栈
- 线程的逻辑代码
线程的执行
抢占式执行:效率高、可防止单一线程长时间独占CPU
线程的创建
Java 中创建线程主要有两种方式
- 继承
Thread类- 实现
Runnable接口
继承 Thread 类
步骤
- 编写类、继承Thread类
- 重写
run()方法 - 创建线程对象
- 调用
start()方法启动线程
public class TortoiseThread extends Thread{
@Override
public void run() {
this.setName("乌龟线程");
while (true) {
System.out.println("乌龟run..." + this.getName());
}
}
}
public class Thread1 {
public static void main(String[] args) {
TortoiseThread tortoiseThread = new TortoiseThread();
tortoiseThread.start();
Thread.currentThread().setName("兔子线程");
while (true) {
System.out.println("兔子run......" + Thread.currentThread().getName());
}
}
}
获取线程名称
getName()Thread.currentThread().getName()
实现 Runnable 接口
步骤
- 创建类实现
Runnable接口,并实现run()方法 - 创建
Runnable实现类对象 - 创建线程对象,传递实现类对象
- 启动线程
public class Task1 implements Runnable{
@Override
public void run() {
Thread.currentThread().setName("跑步");
while (true) {
System.out.println("everyday run" + Thread.currentThread().getName());
}
}
}
public class TestRunnable {
public static void main(String[] args) {
Task1 task1 = new Task1();
Thread thread1 = new Thread(task1);
thread1.start();
Thread.currentThread().setName("呼吸");
while (true) {
System.out.println("everyday breath" + Thread.currentThread().getName());
}
}
}
线程的状态
常用方法
| ⽅法名 | 说明 |
|---|---|
| public static void sleep(long millis) | 当前线程主动休眠 millis 毫秒。 |
| public static void yield() | 当前线程主动放弃时间⽚,回到就绪状态,竞争下⼀次时间⽚。 |
| public final void join() | 允许其他线程加⼊到当前线程中。 |
| public void setPriority(int) | 线程优先级为1-10,默认为5,优先级越⾼,表示获取CPU机会越多 |
| public void setDaemon(boolean) | 设置为守护线程线程有两类:⽤户线程(前台线程)、守护线程(后台线程) |
线程安全
-
线程不安全
- 当多线程并发访问临界资源时,如果破坏原子操作,可能会造成数据不一致
- 临界资源:共享资源(同一对象),一次仅允许一个线程使用,才可保证其正确性
- 原子操作:不可分割的多部操作,被视作一个整体,其顺序和步骤不可打乱或缺省
-
解决方式
- 同步代码块
- 同步方法
同步代码块
- 每个对象都有一个互斥锁标记,用来分配给线程的
- 只有拥有对象互斥锁标记的线程,才能进入该对象加锁的同步代码块
- 线程退出同步代码块时,会释放相应的互斥锁标记
synchronized (临界资源对象) { // 对临界资源对象加锁
// 代码(原子操作)
}
public class Ticket implements Runnable{
private int ticket=100;
@Override
public void run() {
while(true) {
synchronized (this) {//this ---当前对象
if(ticket<=0) {
break;
}
System.out.println(Thread.currentThread().getName()+"卖了第"+ticket+"票");
ticket--;
}
}
}
}
注意:临界资源对象要的是一个共享资源,可能就想到ticket,将它转为包装类Integer,这样不就行了嘛。但是,分析过Integer的底层代码可知道,在[-128,127]范围内Integer返回的是同一个对象,而不在该范围内的数字将重新实例化一个对象,也就是说临界资源对象不是用一个了,那别人也能访问了,这样就达不到互斥的效果了。
同步方法
- 只有拥有对象互斥锁标记的线程,才能进入该对象加锁的同步方法中
- 线程退出同步方法时,会释放相应的互斥锁标记
- 如果方法是静态,锁是
类名.class
public synchronized 返回值类型 方法名() { // 对当前对象(this)加锁
// 代码(原子操作)
}
同步规则
- 只有在调用包含同步代码块的方法,或同步方法,才需要对象的表级锁
- 如调用不包含同步代码块的方法,或普通方法时,则不需要锁标记,可直接调用
死锁
- 当第⼀个线程拥有A对象锁标记,并等待B对象锁标记,同时第⼆个线程拥有B对象锁标记,并等待A对象锁标记时,产⽣死锁。
- ⼀个线程可以同时拥有多个对象的锁标记,当线程阻塞时,不会释放已经拥有的锁标记,由此可能造成死锁。
线程通信
常用方法
所有的等待、通知方法必须在对加锁的同步代码块中
| ⽅法 | 说明 |
|---|---|
| public final void wait() | 释放锁,进⼊等待队列 |
| public final void wait(long timeout) | 在超过指定的时间前,释放锁,进⼊等待队列 |
| public final void notify() | 随机唤醒、通知⼀个线程 |
| public final void notifyAll() | 唤醒、通知所有线程 |
例子
public class Bread {
private int id;
private String productName;
public Bread() {
}
public Bread(String productName, int id) {
super();
this.productName = productName;
this.id = id;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getProductName() {
return productName;
}
public void setProductName(String productName) {
this.productName = productName;
}
@Override
public String toString() {
return "Bread{" +
"id=" + id +
", productName='" + productName + ''' +
'}';
}
}
public class BreadCon {
private final Bread[] cons = new Bread[6];
private int index = 0;
public synchronized void input(Bread b) {
while (index >= 5) {
try {
wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
cons[index] = b;
System.out.println(Thread.currentThread().getName() + "生产了" + b.getId() + " ");
index++;
notifyAll();
}
public synchronized void output() {
while (index <= 0) {
try {
wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
index--;
Bread b = cons[index];
System.out.println(Thread.currentThread().getName() + "消费了" + b.getId() + "生产者:" + b.getProductName());
cons[index] = null;
notifyAll();
}
}
public class Consume implements Runnable{
private BreadCon con;
public Consume(BreadCon con) {
this.con = con;
}
@Override
public void run() {
for (int i = 0; i < 30; i++) {
con.output();
}
}
}
public class Product implements Runnable{
private BreadCon con;
public Product(BreadCon con) {
this.con = con;
}
@Override
public void run() {
for (int i = 0; i < 30; i++) {
con.input(new Bread(Thread.currentThread().getName(), i));
}
}
}
public class Test {
public static void main(String[] args) {
BreadCon con = new BreadCon();
Product product = new Product(con);
Consume consume = new Consume(con);
Thread thread1 = new Thread(product, "明明");
Thread thread2 = new Thread(product, "芳芳");
Thread thread3 = new Thread(consume, "莉莉");
Thread thread4 = new Thread(consume, "敏敏");
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}