Java多线程复习笔记
看完狂神的多线程教程后为了避免遗忘写的复习笔记:
原视频链接
进程(Process)与线程(Thread)
- 程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念;
- 而进程是执行程序的一次执行过程,是一个动态的概念。是系统资源分配的单位;
- 通常在一个进程中可以包含若干个线程,一个进程中至少有一个线程。线程是CPU调度和执行的单位。
- 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为干预的;
- 线程会带来额外的开销,如CPU调度时间,并发控制开销;
- 每个线程在自己的工作内存交互,内存控制不当造成数据不一致。
创建线程的方式
创建线程一共有三种方式,其中继承Thread类和实现Runnable接口更常用一些,推荐使用方式二,这样可以避免了单继承局限的问题。
方式一:继承Thread类
/**
* 创建线程方式一:继承Thread类,覆写run()方法,调用start()开启线程
* 线程开启不一定立即执行,由CPU调度执行
*/
public class TestThread1 extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("测试线程" + i);
}
}
public static void main(String[] args) {
// 创建线程对象
TestThread1 testThread1 = new TestThread1();
// 调用start()方法开启线程
testThread1.start();
// main线程
for (int i = 0; i < 100; i++) {
System.out.println("主线程" + i);
}
}
}
方式二:实现Runnable接口*
/**
* 创建线程方式二:实现Runnable接口,重写run方法,执行线程需要丢入runnable接口实现类,调用start()方法
*/
public class TestThread2 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("线程一" + i);
}
}
public static void main(String[] args) {
// 创建Runnable接口的实现类对象
TestThread2 testThread2 = new TestThread2();
// 创建线程对象,通过线程对象来开启我们的线程,代理
Thread thread = new Thread(testThread2);
thread.start();
for (int i = 0; i < 100; i++) {
System.out.println("线程二" + i);
}
}
}
方式三:实现Callable
import java.util.concurrent.*;
/**
* 创建线程方法三:实现Callable接口
* 1. 可以定义返回值
* 2. 可以抛出异常
*/
public class TestThread4 implements Callable<Boolean> {
@Override
public Boolean call() throws Exception {
for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName() + i);
}
return true;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
TestThread4 testThread1 = new TestThread4();
TestThread4 testThread2 = new TestThread4();
TestThread4 testThread3 = new TestThread4();
// 创建执行服务
ExecutorService ser = Executors.newFixedThreadPool(3);
// 提交执行
Future<Boolean> r1 = ser.submit(testThread1);
Future<Boolean> r2 = ser.submit(testThread2);
Future<Boolean> r3 = ser.submit(testThread3);
// 获取结果
boolean rs1 = r1.get();
boolean rs2 = r2.get();
boolean rs3 = r3.get();
ser.shutdownNow();
}
}
静态代理模式
我们发现方式一继承的Thread类的内部其实也实现了Runnable接口,这里用到了静态代理模式。
以上面方式二中的代码为例,我们自己写的类TestThread2和Thread类都实现了Runnable接口,并且在Thread类内部对Runnable对象进行了接收
此时我们在main方法中就可以通过代理对象(Thread)去代替我们的真实对象(TestThread2)去执行,一方面可以在代理类中自定义许多其他特殊的方法,比如Thread类中的start(),让真实对象更专注于自己本身的职责,另一方面也可以避免对真实对象的直接访问,在一定程度上起到了保护作用。
以下也是一个静态代理模式的简易代码实现:
interface Marry {
void happyMarry();
}
class You implements Marry {
@Override
public void happyMarry() {
System.out.println("Get marry!");
}
}
class WeddingCompany implements Marry {
private Marry client;
public WeddingCompany(Marry client) {
this.client = client;
}
@Override
public void happyMarry() {
before();
this.client.happyMarry(); // 真实对象
after();
}
private void before() {
System.out.println("prepare");
}
private void after() {
System.out.println("packaging");
}
}
public class StaticProxy {
public static void main(String[] args) {
You you = new You();
WeddingCompany weddingCompany = new WeddingCompany(you);
weddingCompany.happyMarry();
}
}
线程状态
public class TestState {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("");
});
// 观察状态
Thread.State state = thread.getState();
System.out.println(state); // NEW
// 观察启动后
thread.start(); // 启动线程
state = thread.getState();
System.out.println(state); // RUN
while (state != Thread.State.TERMINATED) {
Thread.sleep(100);
state = thread.getState(); // 更新线程状态
System.out.println(state);
}
}
}
线程优先级
-
Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行;
-
线程优先级用数字表示,范围1 - 10;
Thread.MIN_PRIORITY = 1;
Thread.MAX_PRIORITY = 10;
Thread.NORM_PRIORITY = 5; // 主线程默认优先级
-
通过以下方式改变或获取优先级
getPriority()
setPriority(int n)
-
优先级的设置应该在start()调度之前。
守护线程
- 线程分为用户线程和守护线程;
- JVM必须确保用户线程执行完毕;
- JVM不用等待守护线程执行完毕;
- 设置方法:
setDaemon(true); // 默认为false
- 常见的守护线程:内存监控,垃圾回收,ms word中的拼写检查。
常用的Thread类方法
线程休眠 sleep()
public class TestSleep {
public static void main(String[] args) {
Date startTime = new Date(System.currentTimeMillis()); // 获取系统当前时间
while (true) {
try {
Thread.sleep(1000);
System.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime));
startTime = new Date(System.currentTimeMillis()); // 更新当前时间
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
线程礼让 yield()
/**
* 测试礼让进程
* 礼让不一定成功
*/
class MyYield implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始执行");
Thread.yield(); // 线程礼让
System.out.println(Thread.currentThread().getName() + "结束执行");
}
}
public class TestYield {
public static void main(String[] args) {
MyYield myYield = new MyYield();
new Thread(myYield, "A").start();
new Thread(myYield, "B").start();
}
}
线程强制执行 join()
/**
* 测试join
* 可以理解为插队
*/
public class TestJoin {
public static void main(String[] args) throws InterruptedException {
Runnable thread1 = null;
thread1 = () -> {
for (int i = 0; i < 1000; i++) {
System.out.println("vip" + i);
}
};
Thread test = new Thread(thread1);
test.start();
// 主线程
for (int i = 0; i < 500; i++) {
if (i == 200) {
test.join();
}
System.out.println("main" + i);
}
}
}
线程同步
Java提供了synchronized关键字(本质上是悲观锁)来解决基础的并发问题,对应有同步代码块和同步方法两种方式。
每个对象都对应一把锁,synchronized只有获得一个对象的锁才能去执行操作该对象的方法,一个锁被获得后,其他的线程需要等待,直到一个线程释放这个锁后才能去获得。下面是两种方法的一些实际运用:
class Account {
int money;
String name;
public Account(int money, String name) {
this.money = money;
this.name = name;
}
}
class Drawing extends Thread {
Account account;
int drawingMoney;
int nowMoney;
public Drawing(Account account, int drawingMoney, String name) {
super(name);
this.account = account;
this.drawingMoney = drawingMoney;
}
/**
* synchronized默认锁的是this
*/
@Override
public void run() {
// 锁的对象就是变化的量,需要增删改的对象
synchronized (account) {
if (account.money - drawingMoney < 0) {
System.out.println(Thread.currentThread().getName() + "钱不够,取不了");
return;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 卡内余额
account.money = account.money - drawingMoney;
// 手里的钱
nowMoney += drawingMoney;
System.out.println(account.name + "余额为:" + account.money);
System.out.println(this.getName() + "手里的钱" + nowMoney);
}
}
}
public class SafeBank {
public static void main(String[] args) {
Account account = new Account(100, "结婚基金");
Drawing you = new Drawing(account, 50, "你");
Drawing npy = new Drawing(account, 100, "NPY");
you.start();
npy.start();
}
}
class BuyTicket implements Runnable {
private int ticketNum = 10;
private boolean flag = true;
@Override
public void run() {
while (flag) {
try {
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// synchronized 同步方法,锁得是this : BuyTicket这个类本身 类是对象的模板,对象是类的实例
private synchronized void buy() throws InterruptedException {
if (ticketNum <= 0) {
flag = false;
return;
}
Thread.sleep(500);
System.out.println(Thread.currentThread().getName() + "买到" + ticketNum--);
}
}
public class SafeBugTicket {
public static void main(String[] args) {
BuyTicket buyTicket = new BuyTicket();
Thread A = new Thread(buyTicket, "A");
A.setPriority(Thread.MIN_PRIORITY);
A.start();
Thread B = new Thread(buyTicket, "B");
B.setPriority(Thread.MAX_PRIORITY);
B.start();
Thread C = new Thread(buyTicket, "C");
C.setPriority(Thread.MAX_PRIORITY);
C.start();
}
}
public class SafeList {
public static void main(String[] args) throws InterruptedException {
List<String> arrayList = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
new Thread(() -> {
synchronized (arrayList) {
arrayList.add(Thread.currentThread().getName());
}
}).start();
}
Thread.sleep(3000);
System.out.println(arrayList.size());
}
}
锁
-
死锁
指多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情景。某一个代码块同时拥有两个以上对象的锁时,就可能会发生死锁问题。
-
通过JUC的Lock来实现线程同步
import java.util.concurrent.locks.ReentrantLock; class Station implements Runnable { private int ticketNum = 10; private final ReentrantLock reentrantLock = new ReentrantLock(); // 本质也是悲观锁 @Override public void run() { while (true) { try { reentrantLock.lock(); // 锁的是当前这个类 if (ticketNum > 0) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(ticketNum--); } else { break; } } finally { reentrantLock.unlock(); // 释放锁 } } } } public class TestLock { public static void main(String[] args) { Station station = new Station(); new Thread(station, "A").start(); new Thread(station, "B").start(); new Thread(station, "C").start(); } }
-
synchronized和Lock的对比
- Lock是显式锁需要手动释放,synchronized是隐式锁,出了作用域自动释放;
- Lock只有代码块锁;
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类);
- 优先使用顺序
- Lock > 同步代码块 > 同步方法
生产者消费者问题(有限缓冲问题 bounded-buffer problem)
存在一个公共且有限的缓冲区,使用这些的进程被分为生产者和消费者,生产者只有在缓冲区未满时生产,消费者只有在缓冲区非空时消费。
管程法
/**
* 生产者消费者模型,利用缓冲区解决:管程法
*/
// 生产者
class Producer extends Thread {
SynContainer container;
public Producer(SynContainer container) {
this.container = container;
}
// 生产
@Override
public void run() {
for (int i = 0; i < 100; i++) {
container.push(new Chicken(i));
System.out.println("生产了" + i + "只鸡");
}
}
}
// 消费者
class Consumer extends Thread {
SynContainer container;
public Consumer(SynContainer container) {
this.container = container;
}
// 消费
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("消费了" + container.pop().id + "只鸡");
}
}
}
// 产品
class Chicken {
int id;
public Chicken(int id) {
this.id = id;
}
}
// 缓冲区
class SynContainer {
// 需要一个容器大小
Chicken[] chickens = new Chicken[10];
// 容器计数器
int count;
// 生产者放入产品
public synchronized void push(Chicken chicken) {
// 如果容器满了,就要等待消费者消费
if (count == chickens.length) {
// 通知消费者消费,生产者等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 如果没有满,就放入产品
chickens[count] = chicken;
count++;
// 可以通知消费者消费了
this.notifyAll();
}
// 消费者消费产品
public synchronized Chicken pop() {
// 判断是否可以消费
if (count == 0) {
// 等待生存者生产,消费者等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 如果可以消费
count--;
Chicken chicken = chickens[count];
// 吃完了,通知生产者生产
this.notifyAll();
return chicken;
}
}
public class TestPC {
public static void main(String[] args) {
SynContainer synContainer = new SynContainer();
new Producer(synContainer).start();
new Consumer(synContainer).start();
}
}
信号灯法
/**
* 信号灯法:设置标志位
*/
// 生产者
class Player extends Thread {
TV tv;
public Player(TV tv) {
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
if (i % 2 == 0) {
this.tv.play("快乐大本营");
} else {
this.tv.play("bilibili");
}
}
}
}
// 消费者
class Watcher extends Thread {
TV tv;
public Watcher(TV tv) {
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
tv.watch();
}
}
}
// 产品
class TV {
/*
演员表演,观众等待 T
观众观看,演员等待 F
*/
String voice; // 表演的节目
boolean flag = true;
// 表演
public synchronized void play(String voice) {
if (!flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("演员表演了" + voice);
// 通知观众观看
this.notifyAll();
this.voice = voice;
this.flag = !this.flag;
}
// 观看
public synchronized void watch() {
if (flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("观看了:" + this.voice);
// 通知演员表演
this.notifyAll();
this.flag = !this.flag;
}
}
public class TestPC2 {
public static void main(String[] args) {
TV tv = new TV();
new Player(tv).start();
new Watcher(tv).start();
}
}
临界区问题(Critical Section Problem)
-
临界区即是会访问共享资源的代码区(改变共同变量,读写文件等),我们需要控制程序进入这段代码的时机。
-
进入区:控制进入临界区
-
临界区:这之内的代码会访问共享资源
-
退出区:告诉其他进程该进程退出了邻接区
-
-
临界区三个原则
- 互斥(Mutual Exclusion):当一个进程/线程在其临界区中执行时,其他进程/线程不能在其临界区中执行;
- 前进(Progress):如果没有进程/线程在它的临界区执行,如果有一些进程/线程希望进入它们的临界区,那么这些进程/线程中的一个将进入临界区。必须有可能协商下一个进入CS的人是谁;
- 有界等待(Bounded Waiting):没有进程/线程应该永远等待进入临界区。临界区外的进程/线程的等待时间应该是有限的(否则进程/线程可能会遭受饥饿。
-
解决办法
-
Peterson’s solution
进程可以共享一些共同的变量来同步它们的行动
-
需要turn和flag[2]来保证互斥、有界等待,以及前进。