前言
多线程以及同步、锁的底层实现好像是在操作系统这门课程学过,JavaSE部分的多线程只是简单的多线程实现,同步与死锁的使用而不需要我们自己具体实现底层,但是有基础的话理解下来会快很多,之后有时间还是去再看一下。
一、多线程
1. 多线程是什么?
进程是操作系统资源分配的基本单位,每个进程有自己的代码以及数据空间。每个进程都包含一个或多个线程他们之间共享代码以及数据空间可以相互通信,相互配合具体完成部分任务,是CPU调度与执行的基本单位。进程就像是所有生产线与生产资料的集合,线程就像是具体的某个生产线的生产过程,可以多个线程同时进行加快生产进度,但需要保证生产资料的分配以及可能存在的先后关系。
就像是我们运行普通的 Java 程序作为一个进程,内部必定包含 main 函数所在的主线程,以及一些垃圾回收等后台线程。多线程的使用场景有1. 大量资源加载(大文件读取) 2. 较小数据量的多连接(服务器、聊天软件)。
每个线程运行时都需要抢占CPU,对于多核的机器来讲可以同时并行运行多个线程,但是线程多核心少时,就需要抢占的并发加并行的执行了。
多个人干同时干多个事(并行)/ 一个人同时做多个事情、一边一边(并发)
2. Java 多线程实现
Thread类的方法:
void start() -> 开启线程,jvm自动调用run方法
void join() -> 插入线程或者叫做插队线程
void wait(long millis) -> 线程等待,释放拥有的锁
void notify() -> 随机唤醒一个正在等待的线程
void notifyAll() -> 唤醒所有正在等待的线程
void run() -> 设置线程任务,这个run方法是Thread重写的接口Runnable中的run方法
String getName() -> 获取线程名字
int getPriority() -> 获取线程优先级
void setName(String name) -> 给线程设置名字
void setPriority(int newPriority) -> 设置线程优先级,优先级越高的线程,抢到CPU使用权的几率越大,但是不是每次都先抢到
int getPriority() -> 获取线程优先级
void setDaemon(boolean on)-> 设置线程为守护进程,例如聊天框关闭文件传输也就结束,可以设置文件传输进程为守护进程
static Thread currentThread() -> 获取正在执行的线程对象(此方法在哪个线程中使用,获取的就是哪个线程对象)
static void sleep(long millis)->线程睡眠,超时后自动醒来继续执行,传递的是毫秒值
static void yield()->礼让,在run的功能代码结束后礼让CPU使用权使进程之间尽可能平衡一些
sleep与wait区别,wait 会释放进程拥有的锁可以用 notify 主动唤醒,sleep 则不会释放进程的锁。下图是线程生命周期内可能的状态转换:
多线程的实现:自定义类继承Thread类并重写run方法或者自定义类实现Runnable接口并重写run方法,再将自定义的类对象作为Thread的构造参数创建Thread对象。(若是不涉及数据可以直接使用匿名内部类一步完成)
// 继承Thread 类
public class MyTicket1 extends Thread {
//定义100张票
static int ticket = 12;
@Override
public void run() {
while (ticket >= 0) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "买了第" + ticket + "张票");
ticket--;
}
}
}
}
// 实现Runnable接口
public class MyTicket2 implements Runnable {
//定义100张票
int ticket = 12;
@Override
public void run() {
while (ticket >= 0) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "买了第" + ticket + "张票");
ticket--;
}
}
}
}
public class Main {
public static void main(String[] args) {
SellTicket1 st1 = new SellTicket1();
SellTicket1 st2 = new SellTicket1();
st1.start();
st2.start();
SellTicket2 st3 = new SellTicket2();
Thread t1 = new Thread(st3);
Thread t2 = new Thread(st3);
t1.start();
t2.start();
}
}
可以看到由于继承的模式每个进程对应一个实际的类对象,因此共享的属性 ticket 需要是静态的,而实现的模式则是多个进程对应一个实际的类对象因此不需要静态修饰。
线程安全问题 修改或访问共享资源在多线程条件下,需要保证一个线程在修改共享数据时其他线程不能同时访问,要一个一个来,因此 Java 通过 synchronized 对涉及数据修改的部分代码(代码块或单独分出函数)进行修饰来实现同步。用 synchronized 修饰普通方法默认锁是 this(进程对应的实例对象)修饰静态方法默认锁就是类名.class(类的class对象对整个类来说唯一,针对继承模式多对象实例)。修饰方法的默认锁无法被修改,修饰代码块则可以自己选择锁。
public class SellTicket1 extends Thread {
//定义100张票
static int ticket = 12;
@Override
public void run() {
while (ticket >= 0) {
sell();
}
}
public synchronized static void sell() {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "卖了第" + ticket + "张票");
ticket--;
}
}
}
public class SellTicket1 extends Thread {
//定义100张票
static int ticket = 12;
@Override
public void run() {
while (ticket >= 0) {
synchronized (SellTicket1.class) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "卖了第" + ticket + "张票");
ticket--;
}
}
}
}
}
上述两种方式是等效的,实现 Runnable 接口的同步方式类似,只不过使用 this 作为代码块的锁就可以,也没必要将同步方法设置为静态方法。
3. 死锁
造成死锁的原因:线程之间竞争同步锁(锁嵌套)
synchronized(b){
synchronized(a){ //此同步代码块在另一同步代码块里
...
}
}
synchronized(a){
synchronized(b){ //此同步代码块在另一同步代码块里
...
}
}
像上述这样在占有一个锁不放弃的同时申请下一个锁,两个线程都占有一个锁且都需要另一个线程占有的锁,双方都不会放弃已有的锁,这样就阻塞形成死锁了。大概了解死锁以及造成死锁的原因,在工作中避免写造成锁嵌套的代码就可以了,更多内容有时间可以再去看操作系统的这一部分内容。