经常在Java岗位详情中有看到"熟悉io、多线程、集合等基础框架"的要求,而自己之前在学习SE部分的时候也只是简单的过了一遍,谈不上"熟悉"二字,所以趁着空闲的时候回过头来学习。以下内容是我在学习关于多线程内容时候的笔记总结,学习视频为b站up主 狂神说Java 的多线程详解 p1-p28。如有错误请指正,谢谢。
总而言之,多线程编程是为了满足程序员编写高效率的程序来达到充分利用 CPU 的目的。
进程:操作系统管理的基本单元。进程包括由操作系统分配的内存空间,包含一个或多个线程。一个进程中可以并发多个线程,每条线程并行执行不同的任务。进程运行直到所有的非守护线程都结束运行后才能结束。
线程:进程中一个单一顺序的控制流,并且一个线程不能独立的存在,它必须是进程的一部分。
如下图活动监视器可以看到进程与线程:
Java提供了三种创建线程的方法:
(插一句嘴:之前在面试的时候也有想到过线程池算不算创建线程的一种方法,在对线程有了初步的了解之后个人认为是不算的。)
自定义线程类继承Thread类,重写run()方法编写线程执行体,创建线程对象,调用start()方法启动线程。
TestThread.java
//总结:注意线程开启不一定立即执行而是有cpu调度执行
public class TestThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println("TestThread " + i);
}
}
public static void main(String[] args) {
//main线程
TestThread testThread = new TestThread();
testThread.start();
for (int i = 0; i < 1000; i++) {
System.out.println("mainThread " + i);
}
}
}
控制台输出:
而如果把testThread线程方法改成run()则先执行完testThread线程方法在执行剩下的方法。如下图诠释:
实现Runnable接口具有多线程能力,启动线程:Thread对象.start(传入目标)。这样使用相对于继承Thread类避免OOP单继承局限性,灵活方便一个对象被多个线程使用。
MyRunnable.java
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println("TestThread " + i);
}
}
public static void main(String[] args) {
//main线程
TestThread testThread = new TestThread();
//创建线程对象,通过线程对象来开启线程,代理模式
new Thread(testThread).start();
for (int i = 0; i < 1000; i++) {
System.out.println("mainThread " + i);
}
}
}
实现Callbale接口需要返回值类型;重写call方法需要抛出异常。
MyCallable.java
public class MyCallable implements Callable<Boolean> {
@Override
public Boolean call() {
System.out.println("MyCallable call...");
return true;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable testThread = new MyCallable();
ExecutorService service = Executors.newFixedThreadPool(1);
Future<Boolean> r = service.submit(testThread);
Boolean rs = r.get();
System.out.println(rs);
System.out.println("mainThread...");
service.shutdownNow();
}
}
源码:
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
不推荐使用JDK提供的stop()、destroy()方法而且已废弃,建议使用一个标志为进行终止变量,当flag=false的时候则终止线程进行。
TestStop.java
public class TestStop implements Runnable{
//1.设置一个标志位
private boolean flag = true;
@Override
public void run() {
int i = 0;
while (flag){
System.out.println("thread run..." + i++);
}
}
//2.设置一个公开的方法停止线程,转换标志位
public void stop(){
this.flag = false;
}
public static void main(String[] args) {
TestStop testStop = new TestStop();
new Thread(testStop).start();
for (int i = 0; i < 1000; i++) {
System.out.println("main thread..." + i);
if(i==900){
//调用stop方法切换标志位让线程停止
testStop.stop();
System.out.println("线程停止了...");
}
}
}
}
sleep(时间)指定当前线程阻塞的毫秒数;sleep时间达到后线程进入就绪状态;sleep可以模拟网络延时,倒计时等;每个对象都有一个锁,而sleep不会释放锁。
案例:模拟倒计时
public class TestSleep2 {
public static void main(String[] args) {
try {
turnDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//模拟倒计时
public static void turnDown() throws InterruptedException {
int num = 10;
while(true){
Thread.sleep(1000);
System.out.println(num--);
if(num<=0){
break;
}
}
}
}
让当前正在执行的线程暂停,但不阻塞,从运行状态转为就绪状态,让cpu重新调度,礼让不一定成功。
TestYield.java
public class TestYield {
public static void main(String[] args) {
MyYield myYield = new MyYield();
new Thread(myYield,"a").start();
new Thread(myYield,"b").start();
}
}
class MyYield implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"线程开始执行");
Thread.yield();
System.out.println(Thread.currentThread().getName()+"线程停止执行");
}
}
Join合并线程,待此线程执行完成后再执行其它线程,其它线程阻塞。
TestJoin.java
//线程强制执行相当于插队
public class TestJoin implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("线程VIP..."+i);
}
}
public static void main(String[] args) throws InterruptedException {
TestJoin testJoin = new TestJoin();
Thread thread = new Thread(testJoin);
thread.start();
//主线程
for (int i = 0; i < 200; i++) {
if (i==100){
thread.join();
}
System.out.println("main "+i);
}
}
}
Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。
线程优先级用数字来表示,范围从1~10。优先级高低只是意味着获得调度的概率高低,都是看CPU的调度。
/**
* The minimum priority that a thread can have.
*/
public final static int MIN_PRIORITY = 1;
/**
* The default priority that is assigned to a thread.
*/
public final static int NORM_PRIORITY = 5;
/**
* The maximum priority that a thread can have.
*/
public final static int MAX_PRIORITY = 10;
//更改优先级
public final void setPriority(int newPriority) {
//获得优先级
public final int getPriority() {
线程分为用户线程和守护线程。虚拟机必须确保用户线程执行完毕;不用等待守护线程是否执行完毕。常见的守护线程如:后台操作记录日志、监控内存、垃圾回收等等。
案例:
//测试守护线程:上帝保佑
public class TestDaemon {
public static void main(String[] args) {
God god = new God();
You you = new You();
Thread thread = new Thread(god);
//默认是false表示用户线程,而正常的线程都是用户线程
thread.setDaemon(true);
thread.start();
new Thread(you).start();
}
}
class God implements Runnable{
@Override
public void run() {
while (true){
System.out.println("God bless you");
}
}
}
class You implements Runnable{
@Override
public void run() {
for (int i = 0; i < 36500; i++) {
System.out.println("live...");
}
System.out.println("go die...");
}
}
当多个线程访问、操作同一资源,并且某些线程还想修改这个对象,这时候就需要线程同步。线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进行这个对象的等待池形成队列,等待前面的线程使用完毕后下一个线程再使用。
案例:简化版抢票
//多个线程同时操作同一对象
public class TestThread4 implements Runnable {
private int ticketNums = 10;
@Override
public void run() {
while (true){
if(ticketNums <= 0){
break;
}
//模仿网络延时
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"-->抢到了第"+ ticketNums-- +"张票");
}
}
public static void main(String[] args) {
TestThread4 ticket = new TestThread4();
new Thread(ticket,"a").start();
new Thread(ticket,"b").start();
new Thread(ticket,"c").start();
}
}
由于同一个进程的多个线程共享同一块存储空间,在带来方便的同时也带来访问冲突问题,为了确保数据在方法中被访问时的正确性,在访问时加入锁机制 synchronized,当一个线程获得对象的排它锁,独占资源,其它线程必须等待,使用后释放锁即可,但是也存在以下问题:
synchronized关键字包括两种用法:synchronized 方法和 synchronized 块。
案例:简化版优化抢票(synchronized 方法)
public class UnsafeBuyTicket {
public static void main(String[] args) {
BuyTicket station = new BuyTicket();
new Thread(station,"a").start();
new Thread(station,"b").start();
new Thread(station,"黄牛党").start();
}
}
class BuyTicket implements Runnable{
private int ticketNums = 10;
boolean flag = true;
@Override
public void run() {
while (flag){
try {
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private synchronized void buy() throws InterruptedException {
if(ticketNums <= 0){
flag = false;
return;
}
//模拟延迟
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"抢到了第"+ticketNums--+"张票");
}
}
同步块:synchronized (Obj){ }
Obj称之为同步监视器,Obj可以是任何对象,但是推荐使用共享资源作为同步监视器;同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身或者是class。
同步监视器的执行过程:
当某个同步块中同时拥有"两个以上对象的锁"时,从而导致两个或多个线程都在等待对方释放资源都停止执行的"死锁"情况。
案例:灰姑娘和白雪公主去争夺口红和镜子-_-
public class DeadLock {
public static void main(String[] args) {
Makeup girl1 = new Makeup(0,"灰姑娘");
Makeup gril2 = new Makeup(1,"白雪公主");
girl1.start();
gril2.start();
}
}
//口红
class Lipstick{
}
//镜子
class Mirror{
}
class Makeup extends Thread{
//用static确保只有一份
static Lipstick lipstic = new Lipstick();
static Mirror mirror = new Mirror();
//选择
int choice;
//女孩名字
String girlName;
Makeup(int choice,String girlName){
this.choice = choice;
this.girlName = girlName;
}
@Override
public void run() {
try {
makeup();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void makeup() throws InterruptedException {
if(choice == 0){
synchronized (lipstic){
System.out.println(this.girlName+"获得口红的锁");
Thread.sleep(1000);
synchronized (mirror){
System.out.println(this.girlName+"获得镜子的锁");
}
}
}else {
synchronized (mirror){
System.out.println(this.girlName+"获得镜子的锁");
Thread.sleep(2000);
synchronized (lipstic){
System.out.println(this.girlName+"获得口红的锁");
}
}
}
}
}
控制台输出:程序无法正常停止,但是把程序中的两个synchronized块里面的另一个synchronized块拿出即可解决。
Java提供更强大的线程同步机制:通过显式定义同步锁对象来实现同步。同步锁使用 Lock 对象充当。
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
ReentrantLock类实现了Lock,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常见用的是 ReentrantLock,可以显式加锁、释放锁。
案例:简化版抢票
public class TestLock {
public static void main(String[] args) {
TestLock2 testLock2 = new TestLock2();
new Thread(testLock2).start();
new Thread(testLock2).start();
new Thread(testLock2).start();
}
}
class TestLock2 implements Runnable{
int ticketNums = 10;
//定义lock锁
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true){
try{
//加锁
lock.lock();
if(ticketNums > 0){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ticketNums--);
}else {
break;
}
}finally {
//解锁
lock.unlock();
}
}
}
}
生产者和消费者共享一个资源,并且生产者和消费者之间互相依赖也互为条件。对于生产者来说,没有生产之前要通知消费者等待;在生产之后又要通知消费者消费。对于消费者来说,在消费之后要通知生产者已经结束消费。
在生产者消费者问题中,仅有 synchnized 是不够的,是因为 synchnized可阻止并发更新同一个资源但是不能用来实现不同线程之间的信息传递。