多线程
基本概念: 程序, 进程和线程
程序: 静态的代码
进程: 程序的一次运行过程. 有产生, 存在和消亡的过程, 即生命周期
线程: 一个进程可以并行执行多个线程. 系统会为每个进行分配内存区域, 而进程中每个线程有自己独立的运行栈和程序计数器, 线程之间共享堆空间和方法区
线程状态State
wait和sleep的区别
wait只能在同步代码块中调用, 必须释放锁.
sleep不会释放锁, 因此可以在任何地方调用.
线程的创建(重点)
继承Thread类
- 创建一个继承于
Thread的类 - 重写
run方法 - 调用
start方法: 启动线程并执行run方法
public class MyThread extends Thread {
private static int count = 100;
@Override
public void run() {
while (true) {
if (count > 0) {
System.out.println(currentThread().getName() + ":" + count--);
} else {
break;
}
}
}
public static void main(String[] args) {
MyThread thread01 = new MyThread();
MyThread thread02 = new MyThread();
MyThread thread03 = new MyThread();
thread01.setName("窗口1");
thread02.setName("窗口2");
thread03.setName("窗口3");
thread01.start();
thread02.start();
thread03.start();
}
}
实现Runnable接口
- 创建一个实现了
Runnable接口的类 - 实现类去实现接口中的
run方法 - 创建实现类的对象, 并以该对象作为
Thread类含参构造器的参数构造Thread类的对象 - 调用
start方法
public class MThread implements Runnable {
/**
* 使用Runnable接口的方式只生成了一个对象, 因此多个线程共用一个对象中的数据
*/
private int count = 100;
@Override
public void run() {
while (true) {
if (count > 0) {
System.out.println(Thread.currentThread().getName() + ":" + count--);
} else {
break;
}
}
}
public static void main(String[] args) {
//只生成一个MThread类的对象
Runnable mThread = new MThread();
//通过一个相同的对象生成多个线程
Thread thread01 = new Thread(mThread, "线程1");
Thread thread02 = new Thread(mThread, "线程2");
thread01.start();
thread02.start();
}
}
两种方式的比较
实现Runnable接口更适合用来创建多线程,
- 一是没有单继承的局限性,
- 二是能天然地体现出共享数据的概念, 在继承Thread类的方法中共享数据和锁都需要使用
static进行修饰, 否则不唯一
两种方式都需要重写
run方法
线程的生命周期
Thread.State记录了线程的生命周期的状态
- 新建:
new线程对象 - 就绪: 执行
start后, 等待cpu调用 - 运行: 占用cpu, 执行
run方法体中的内容yield会从运行态返回到就绪态sleep, join, wait, suspend等方法以及等待同步锁会使得当前进程阻塞
- 堵塞: 比如一些打印输出功能
- 死亡: 调用
stop方法, 或者执行完run方法
线程的同步/并发安全问题(重点)
各种实现方法之间有着细微差异, 但核心是保证用同一把锁, 需要区分
this所指代的是什么对象
- 同步代码块
- 同步方法
- Lock同步锁
同步代码块synchronized
- 锁: 任何一个类的对象都可以作为锁. 要求多个线程共用同一把锁
- 临界区: 需要被同步的代码, 操作共享数据
优缺点
- 优点: 解决了线程安全问题
- 局限性: 操作临界区部分的代码相当于单线程, 稍微降低了一些速度
同步代码块显然不能包含太少代码, 同时也不能包含太多代码, 否则可能会导致逻辑发生变化, 比如下面案例中的while和synchronized互换
public class MThread implements Runnable {
/**
* 使用Runnable接口的方式只生成了一个对象, 因此多个线程共用一个对象中的数据
*/
private int count = 100;
/**
* 需要保证不同的线程共用同一把锁, 使用Runnable接口实现类的方式其实可以不用该
*/
final Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (MThread.class) {
// 某种具体的类也是一个对象, 是更高层次抽象的类的一个对象
// synchronized(this){
// synchronized(obj){
if (count > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + count--);
} else {
break;
}
}
}
}
public static void main(String[] args) {
//只生成一个MThread类的对象
MThread mThread = new MThread();
//通过一个相同的对象生成多个线程
Thread thread01 = new Thread(mThread, "线程1");
Thread thread02 = new Thread(mThread, "线程2");
thread01.start();
thread02.start();
}
}
同步方法
使用synchronized关键字对方法名进行修饰. 对于非static方法, 锁默认为this, 而对于static方法, 锁默认为的类名.class
Lock同步锁
lock方式需要手动的申请和释放锁, 而synchronized方式自动释放锁
import java.util.concurrent.locks.ReentrantLock;
public class LockTest implements Runnable {
private int count = 100;
/**
* 实例化lock
*/
ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
// lock()和unlock()必须成对使用, lock在try前面, unlock在finally里面
// 获取锁
lock.lock();
try {
if (count > 0) {
System.out.println(Thread.currentThread().getName() + ":" + count--);
} else {
break;
}
} finally {
lock.unlock();
}
}
}
}
死锁问题
隐蔽的死锁问题经常是同步方法中调用其它对象的同步方法, 每次执行同步方法都要获取当前对象, 先后需要两个对象及以上作为锁便满足死锁条件
避免死锁的策略
- 避免嵌套锁
- 尽量少的使用同步资源