线程简介
普通方法调用和多线程
普通方法调用:主线程调用run()
方法,主线程执行run()
;只有主线程一条执行路径
多线程:主线程调用start()
方法,子线程执行run()
;多条执行路径,主线程和子线程并行交替执行
程序、进程Process和线程Thread
进程是程序的一次执行过程,是一个动态的概念。进程是资源分配的单位。
很多多线程是模拟出来的,真正的多线程是指有多个CPU,如服务器。
在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,gc线程
main()
称之为主线程,为系统的入口,用于执行整个程序
在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为干预的。
对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制
线程会带来额外的开销,如CPU调度时间,并发控制开销
每个线程在自己的工作内存交互,内存控制不当会造成数据不一致
线程创建
三种创建方式:
Runnable interface:实现Runnable接口
Thread class:继承Thread类
Thread类就实现了Runnable接口
Callable interface:实现Callable接口(初步阶段仅作了解)
线程创建后不一定立即执行,CPU负责安排调度。
继承Thread类
自定义线程类继承Thread类
重写run()
方法,编写线程执行体
创建线程对象,调用start()
方法启动线程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public class TestThread1 extends Thread { @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println("我在看代码---" + i); } }
public static void main(String[] args) {
TestThread1 testThread1 = new TestThread1();
testThread1.start();
for (int i = 0; i < 100; i++) { System.out.println("我在学习多线程--" + i); } } }
|
线程实践——网图下载
- 搜索
commons-io
- 在apache官网下载 Apache Commons IO-bin的压缩包
- 解压后复制
commons-io-2.8.0.jar
- 打开IDEA,在合适目录层级下new package –
lib
- 在生成的lib下粘贴
- 右键package lib,选择
Add as Library
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| import org.apache.commons.io.FileUtils;
import java.io.File; import java.io.IOException; import java.net.URL;
public class TestThread2 extends Thread { private String url; private String name;
public TestThread2(String url, String name) { this.url = url; this.name = name; }
@Override public void run() { WebDownloader webDownloader = new WebDownloader(); webDownloader.downloader(url, name); System.out.println("下载了文件名为:" + name); }
public static void main(String[] args) { String str = "http://p3-q.mafengwo.net/s13/M00/5A/FE/wKgEaVyYzWqAZIvdABtU3xfCwTg13.jpeg"; TestThread2 t1 = new TestThread2(str, "douban1.jpg"); TestThread2 t2 = new TestThread2(str, "douban2.jpg"); TestThread2 t3 = new TestThread2(str, "douban3.jpg");
t1.start(); t2.start(); t3.start(); } }
class WebDownloader { public void downloader(String url, String name) { try { FileUtils.copyURLToFile(new URL(url), new File(name)); } catch (IOException e) { e.printStackTrace(); System.out.println("IO异常,downloader方法出现问题"); } } }
|
实现Runnable接口(推荐)
- 自定义类实现Runnable接口
- 实现
run()
方法,编写线程执行体
- 创建线程对象,丢入Runnable接口实现类,调用
start()
方法启动线程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public class TestThread3 implements Runnable {
@Override public void run() { for (int i = 0; i < 100; i++) { System.out.println("我在看代码---" + i); } }
public static void main(String[] args) { TestThread3 testThread3 = new TestThread3();
new Thread(testThread3).start();
for (int i = 0; i < 100; i++) { System.out.println("我在学习多线程--" + i); } } }
|
继承Thread类和实现Runnable接口的比较
继承Thread类
- 子类继承Thread类具备多线程能力
- 启动线程的方式:子类对象.strat()
- 不建议使用:避免OOP(Object-Oriented Programming)单继承的局限性
实现Runnable接口
线程实践 - 龟兔赛跑
- 创建赛道
- 判断比赛是否结束
- 输出胜利者
- 龟兔赛跑开始
- 模拟兔子睡觉
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
|
public class Race implements Runnable {
private static String winner;
@Override public void run() { for (int i = 1; i <= 100; i++) { if (Thread.currentThread().getName().equals("兔子") && i % 10 == 0) { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } }
boolean flag = gameOver(i); if (flag) { break; } System.out.println(Thread.currentThread().getName() + "-->跑了" + i + "步"); } }
private boolean gameOver(int steps) { if (winner != null) { return true; } else if (steps >= 100) { winner = Thread.currentThread().getName(); System.out.println("winner is " + winner); return true; } return false; }
public static void main(String[] args) { Race race = new Race();
new Thread(race, "兔子").start(); new Thread(race, "乌龟").start();
} }
|
实现Callable接口(了解)
- 实现Callable接口,需要返回值类型
- 重写call方法,需要抛出异常
- 创建目标对象
- 创建执行服务:
ExecutorService ser = Executors.newFIxedThreadPool(1);
- 提交执行:
Future<Boolean> result1 = ser.submit(t1);
- 获取结果:
boolean r1 = result1.get()
- 关闭服务:
ser.shutdownNow();
线程实践——网图下载改写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| public class TestCallable implements Callable<Boolean> { private String url; private String name;
public TestCallable(String url, String name) { this.url = url; this.name = name; }
@Override public Boolean call() { WebDownloader webDownloader = new WebDownloader(); webDownloader.downloader(url, name); System.out.println("下载了文件名为:" + name); return true; }
public static void main(String[] args) throws ExecutionException, InterruptedException { String str = "http://p3-q.mafengwo.net/s13/M00/5A/FE/wKgEaVyYzWqAZIvdABtU3xfCwTg13.jpeg"; TestCallable t1 = new TestCallable(str, "douban1.jpg"); TestCallable t2 = new TestCallable(str, "douban2.jpg"); TestCallable t3 = new TestCallable(str, "douban3.jpg");
ExecutorService ser = Executors.newFixedThreadPool(3);
Future<Boolean> r1 = ser.submit(t1); Future<Boolean> r2 = ser.submit(t2); Future<Boolean> r3 = ser.submit(t3);
boolean rs1 = r1.get(); boolean rs2 = r2.get(); boolean rs3 = r3.get();
System.out.println(rs1); System.out.println(rs2); System.out.println(rs3);
ser.shutdownNow(); } }
class WebDownloader { public void downloader(String url, String name) { try { FileUtils.copyURLToFile(new URL(url), new File(name)); } catch (IOException e) { e.printStackTrace(); System.out.println("IO异常,downloader方法出现问题"); } } }
|
Lambda 表达式
实质是属于函数式编程的概念
(params) -> expression [表达式]
(params) -> statement [语句]
(params) -> {statements}
1
| new Thread(() -> System.out.println("多线程学习")).start();
|
理解Functional Interface(函数式接口)是学习Java 8 Lambda表达式的关键。
为什么使用Lambda表达式
- 避免匿名内部类定义过多
- 使代码简洁
- 去掉了一堆没有意义的代码,只留下核心逻辑
Lambda表达式的推导
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
|
public class TestLambda1 {
static class Like2 implements Ilike { @Override public void lambda() { System.out.println("I like lambda2"); } }
public static void main(String[] args) { Ilike like = new Like(); like.lambda();
like = new Like2(); like.lambda();
class Like3 implements Ilike { @Override public void lambda() { System.out.println("I like lambda3"); } } like = new Like3(); like.lambda();
like = new Ilike() { @Override public void lambda() { System.out.println("I like lambda4"); } }; like.lambda();
like = () -> { System.out.println("I like lambda5"); }; like.lambda(); } }
interface Ilike { void lambda(); }
class Like implements Ilike { @Override public void lambda() { System.out.println("I like Lambda"); } }
|
Lambda表达式的简化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| public class TestLambda2 { public static void main(String[] args) { Ilove love = (int a) -> { System.out.println("I love you-->" + a); };
love = (a) -> { System.out.println("I love you-->" + a); };
love = a -> { System.out.println("I love you-->" + a); };
love = a -> System.out.println("I love you-->" + a);
love.love(1000); } }
interface Ilove { void love(int a); }
|
总结
使用Lambda表达式创建接口的对象,接口必须是函数式接口
可以去掉参数类型
有多个参数时,显式/隐式声明参数类型需要统一
只有1个参数时,参数外的括号可以省略
静态代理模式 static proxy
- 真实对象和代理对象都要实现同一个接口
- 代理对象要代理真实角色
好处:
- 代理对象可以完成很多真实对象无法处理的事情
- 代理对象专注做自己的事情
用婚庆公司和新人来举例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| public class StaticProxy { public static void main(String[] args) { new WeddingCompany(new You()).happyMarry();
new Thread(() -> System.out.println("我爱你")).start(); } }
interface Marry { void happyMarry(); }
class You implements Marry { @Override public void happyMarry() { System.out.println("死生契阔,与子成说"); } }
class WeddingCompany implements Marry { private Marry target;
public WeddingCompany(Marry target) { this.target = target; }
@Override public void happyMarry() { before(); this.target.happyMarry(); after(); }
private void before() { System.out.println("布置现场"); }
private void after() { System.out.println("收尾款"); } }
|
线程状态
- NEW 创建
尚未启动(未执行start方法)的线程处于此状态
RUNNABLE 运行
在Java虚拟机中执行的线程处于此状态
BLOCKED 阻塞
被阻塞等待监视器锁定的线程处于此状态
WAITING 等待
正在等待另一个线程执行特定动作的线程处于此状态
TIMED_WAITING 超时等待(过期不候)
正在等待另一个线程执行动作达到指定等待时间的线程处于此状态
TERMINATED 终止
已退出的线程处于此状态
观测线程状态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| 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);
thread.start(); state = thread.getState(); System.out.println(state);
while (state != Thread.State.TERMINATED) { Thread.sleep(100); state = thread.getState(); System.out.println(state); } } }
|
线程方法
方法 |
说明 |
setPriority(int newPriority) |
更改线程的优先级 |
static void sleep(long millis) |
让正在执行的线程休眠指定的毫秒数 |
void join() |
线程强制执行(直接获得最高优先级,并将完成执行) |
static void yield() |
暂停当前正在执行的线程对象,并执行其他线程 |
void interrupt() |
中断线程,但不建议用这个方法 |
boolean isAlive() |
检查线程是否处于活动状态 |
停止线程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| public class TestStop implements Runnable {
private boolean flag = true;
@Override public void run() { int i = 0; while (flag) { System.out.println("run...Thread" + i++); } }
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" + i); if (i == 900) { testStop.stop(); System.out.println("线程该停止了"); } } } }
|
线程休眠
- sleep指定当前线程阻塞的毫秒数
- sleep存在异常InterruptedException
- sleep时间达到后,线程进入就绪状态
- sleep可以模拟网络延时,模拟倒计时等
- 每一个对象都有一个锁,sleep不会释放锁
实际使用过程中,不会直接调用Thread的sleep方法,而是会通过java.util.Concurrent.TimeUnit
调用sleep方法(线程安全)。
模拟网络延时
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| package com.hunter.thread.demo04;
import com.hunter.thread.demo01.TestThread4;
public class TestSleep 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 tickets = new TestThread4();
new Thread(tickets, "小明").start(); new Thread(tickets,"张三").start(); new Thread(tickets, "黄牛").start(); } }
|
模拟倒计时/走秒
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| public class TestSleep2 {
public static void tenDown() throws InterruptedException { int num = 10;
while (true) { Thread.sleep(1000); System.out.println(num--); if (num <= 0) { break; } } }
public static void main(String[] args) { try { tenDown(); } catch (InterruptedException e) { e.printStackTrace(); }
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(); } } } }
|
线程礼让
- 让当前正在执行的线程暂停,但不阻塞
- 将线程从运行状态转为就绪状态
- 让CPU重新调度(礼让不一定成功)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| 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() + "线程停止执行"); } }
|
线程强制执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| public class TestJoin implements Runnable {
@Override public void run() { for (int i = 0; i < 1000; 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 < 500; i++) { if (i == 200) { thread.join(); } System.out.println("main" + i); } } }
|
线程的优先级
Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。
线程的优先级用数字表示,范围从1~10:
- Thread.MIN_PRIORITY == 1;
- Thread.MAX_PRIORITY == 10;
- Thread.NORM_PRIORITY == 5;
使用以下方式改变或获取优先级:
getPriority().setPriority(int xxx)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
|
public class TestPriority {
public static void main(String[] args) { System.out.println(Thread.currentThread().getName() + "-->" + Thread.currentThread().getPriority());
MyPriority myPriority = new MyPriority();
Thread t1 = new Thread(myPriority); Thread t2 = new Thread(myPriority); Thread t3 = new Thread(myPriority); Thread t4 = new Thread(myPriority); Thread t5 = new Thread(myPriority); Thread t6 = new Thread(myPriority);
t1.start();
t2.setPriority(1); t2.start();
t3.setPriority(4); t3.start();
t4.setPriority(Thread.MAX_PRIORITY); t4.start();
}
}
class MyPriority implements Runnable {
@Override public void run() { System.out.println(Thread.currentThread().getName() + "-->" + Thread.currentThread().getPriority()); } }
|
线程的优先级低只是获得调度的概率低,并不是优先级低就不会被调用,最终还是看CPU的调度。如果优先级低的线程反而总能优先执行,称为性能倒置,但一般不会发生。
守护线程 deamon
- 线程分为用户线程和守护线程
- 虚拟机必须确保用户线程执行完毕
- 虚拟机不用等待守护线程执行完毕
- 如,后台记录操作日志,监控内存,垃圾回收等待…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
|
public class TestDaemon {
public static void main(String[] args) { God god = new God(); You you = new You();
Thread thread = new Thread(god); 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 < 30000; i++) { System.out.println("你一生都开心地活着"); } System.out.println("=========================goodbye world!========================"); } }
|
线程同步机制
处理多线程问题时,多个线程访问同一个对象(并发),并且某些线程还想修改这个对象,这时候我们就需要线程同步。线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程再使用。
由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题。为了保证数据在方法中被访问时的正确性,在访问时加入锁机制来同步。当一个线程获得对象的排它锁,独占资源,其他线程必须等待使用后释放锁即可。存在以下问题:
- 一个线程持有锁会导致其他所有需要此锁的线程挂起;
- 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题;
- 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题
由于我们可以通过private关键字来保证数据对象只能被方法访问,所以我们只需要针对方法提出一套机制,这套机制就是synchronized关键字,它包括两种用法:synchronized方法和synchronized块。
同步方法
同步方法: public synchronized void method(int args) {}
synchronized方法控制对对象的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。
缺陷:若将一个大的方法申明为synchronized,将会影响效率。
同步块
同步块: synchronized(Obj) {}
Obj称之为同步监视器:
- Obj可以是任何对象,但是推荐使用共享资源作为同步监视器
- 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,或者是class[反射中讲解]
同步监视器的执行过程:
- 第一个线程访问,锁定同步监视器,执行其中代码
- 第二个线程访问,发现同步监视器被锁定,无法访可
- 第一个线程访问完毕,解锁同步监视器
- 第二个线程访问,发现同步监视器没有锁,然后锁定并访问
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public class TestSyn03 { public static void main(String[] args) { List<String> list = new ArrayList<String>(); for (int i = 0; i < 10000; i++) { new Thread(() -> { synchronized (list) { list.add(Thread.currentThread().getName()); } }).start();
} try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(list.size()); } }
|
死锁
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行的阻塞现象。某一个同步块同时拥有两个以上对象的锁时,就可能会发生死锁的问题。
产生死锁的必要条件
- 互斥条件:一个资源每次只能被一个进程使用;
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放;
- 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
只要想办法破解其中任意一个或多个条件,就可以避免死锁。
Lock (锁)
从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步,同步锁使用Lock对象充当。
java.utl.concurrent.locks.Lock
接口是控制多个线程对共享资源进行访问的工具。
锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
ReentrantLock
类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock(Reentrant:可重入),可以显式加锁、释放锁。
常用方法:
lock()
unlock()
trylock()
尝试获取锁
使用方法:
1 2 3 4 5 6 7 8 9 10
| class A { private final ReentrantLock lock = new ReentrantLock(); public void m() { lock.lock(); try { } finally { lock.unlock(); } }
|
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| 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;
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(); } } } }
|
CopyOnWriteArrayList
CopyOnWriteArrayList是java.util.concurrent下的安全类型的集合。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class TestJUC { public static void main(String[] args) { CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>(); for (int i = 0; i < 10000; i++) { new Thread(() -> { list.add(Thread.currentThread().getName()); }).start(); }
try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(list.size()); } }
|
synchronized 与 Lock 的对比
synchronized是内置的java关键字,Lock是一个java类
Lock是显式锁(手动开启和关闭锁,别忘记关闭锁) ;synchronized是隐式锁,出了作用域自动释放。
Lock只有代码块锁, synchronized有代码块锁和方法锁
使用Lock锁,JVM将花费较少的时间来调度线程,性能更好,并且具有更好的扩展性(提供更多的子类)
synchronized是可重入锁,而且是非公平锁;Lock锁也是可重入锁,默认是非公平锁
synchronized适合锁少量的同步代码,lock锁的灵活度非常高,适合锁大量的同步代码
优先使用顺序:
Lock > 同步代码块(已经进入了方法体,分配了相应资源) > 同步方法(在方法体之外)
线程通信
Java提供了几个解决线程之间通信问题的方法
方法名 |
作用 |
wait() |
表示线程一直等待,直到其他线程通知,与sleep不同,会释放锁 |
wait(long timeout) |
指定等待的毫秒数 |
notify() |
唤醒一个处于等待的线程 |
notifyAll() |
唤醒同一个对象上所有调用wait()方法的线程,优先级高的线程优先调度 |
上述方法均是Object类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出IlegalMonitorStateException异常。
生产者消费者问题
- 假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取走消费。
- 如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止
- 如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品为止
这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件。
- 对于生产者,没有生产产品之前,要通知消费者等待。而生产了产品之后,又需要马上通知消费者消费。
- 对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费。
- 在生产者消费者问题中,仅有 synchronized是不够的
- synchronized可阻止并发更新同一个共享资源,实现了同步
- synchronized不能用来实现不同线程之间的消息传递(通信)
synchronized 版
解决方式一:管程法
- 生产者:负责生产数据的模块(可能是方法,对象,线程,进程)
- 消费者:负责处理数据的模块(可能是方法,对象,线程,进程)
- 缓冲区:消费者不能直接使用生产者的数据,他们之间有个缓冲区
生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107
| public class TestProductCustomer {
public static void main(String[] args) { SynContainer container = new SynContainer();
new Producer(container).start(); new Consumer(container).start();
}
}
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("生产了id为" + 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("消费了id为" + container.pop().id + "的鸡"); } } }
class Chicken { int id;
public Chicken(int id) { this.id = id; } }
class SynContainer { Chicken[] chickens = new Chicken[10];
int count = 0;
public synchronized void push(Chicken chicken) { while (count == chickens.length) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } }
chickens[count] = chicken; count++;
this.notifyAll(); }
public synchronized Chicken pop() { while (count == 0) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } }
count--; Chicken chicken = chickens[count];
this.notifyAll();
return chicken; } }
|
解决方式二:信号灯法
利用标识位。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
| public class TestProductCustomer2 { public static void main(String[] args) { TV tv = new TV(); new Player(tv).start(); new Audience(tv).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("进广告"); } } } }
class Audience extends Thread { TV tv; public Audience(TV tv) { this.tv = tv; }
@Override public void run() { for (int i = 0; i < 20; i++) { tv.watch(); } } }
class TV { String show; boolean flag = true;
public synchronized void play(String show) {
if (!flag) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("演员表演了:" + show); this.notifyAll(); this.show = show; this.flag = !this.flag; }
public synchronized void watch() { if (flag) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("观看了:" + show);
this.notifyAll(); this.flag = !this.flag; } }
|
Lock版
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113
| public class TestProductCustomer {
public static void main(String[] args) { SynContainer container = new SynContainer();
new Producer(container).start(); new Consumer(container).start();
}
}
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("生产了id为" + 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("消费了id为" + container.pop().id + "的鸡"); } } }
class Chicken { int id;
public Chicken(int id) { this.id = id; } }
class SynContainer { Chicken[] chickens = new Chicken[10];
int count = 0; Lock lock = new ReentrantLock(); Condition condition = lock.newCondition();
public void push(Chicken chicken) { lock.lock(); try { while (count == chickens.length) { condition.await(); } chickens[count] = chicken; count++;
condition.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } }
public Chicken pop() { lock.lock(); try { while (count == 0) { condition.await(); } count--; Chicken chicken = chickens[count];
condition.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } return chicken; } }
|
线程池
- 背景:经常创建和销毀、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
- 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。
可以避兔频繁创建销毀、实现重复利用。
- 好处:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理
使用线程池
JDK 5.0起提供了线程池相关API:ExecutorService和Executors。
ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
- 构造函数的参数
corePoolSize
:核心池的大小
maximumPoolSize
:最大线程数
keepAliveTime
:线程没有任务时最多保持多长时间后会终止
workQueue
: 一个阻塞队列,用来存储等待执行的任务。
- ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列
- LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列
- SynchronousQueue: 一个不存储元素的阻塞队列
threadFactory
:定义如何启动一个线程,可以设置线程的名称,并且可以确定是否后台线程等。
void execute(Runnable command)
:执行任务/命令,没有返回值,一般用来执行Runnable
<T> Future<T> submit(Callable<T> task)
:执行任务,有返回值,一般用来执行Callable
void shutdown()
:关闭连接池
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池。
Executors.defaultThreadFactory()
为什么要自定义线程池
Executors.newFixedThreadPool()
使用的LinkedBlockingQueue相当于是一个无界队列,因为队列长度限制为Integer.MAX_VALUE,如果瞬间请求非常大,会有OOM的风险。
Executors.newCachedThreadPool()
最大线程数设置为最大的Integer.MAX_VALUE,如果最大线程数maximumPoolSize达到最大,那么会导致OOM异常。
使用自定义线程池ThreadPoolExecutor,可以控制最大线程数、阻塞队列长度,避免OOM。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| public class ThreadPoolUtil {
public static ExecutorService initExecutor() { int corePoolSize = 20; int maximumPoolSize = 100; long keepAliveTime = 2; TimeUnit timeUnit = TimeUnit.SECONDS; BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(200);
ThreadFactory threadFactory = new ThreadFactoryBuilder() .setNameFormat("demo-pool-%d").build();
return new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, timeUnit, workQueue, threadFactory);
} }
|
自定义线程池 - 乐之者v - 博客园 (cnblogs.com)
Java线程池中三种方式创建 ThreadFactory 设置线程名称 - 甜菜波波 - 博客园 (cnblogs.com)
阻塞队列 BlockingQueue
- 当队列中没有数据的情况下,消费者端的所有线程都会被自动阻塞(挂起),直到有数据放入队列。
- 当队列中填满数据的情况下,生产者端的所有线程都会被自动阻塞(挂起),直到队列中有空的位置,线程被自动唤醒。
常见的BlockingQueue
ArrayBlockingQueue
基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了一个定长数组外,ArrayBlockingQueue内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。
BlockingQueue - 不会就问咯 - 博客园 (cnblogs.com)
ThreadLocal
ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题。
Java中的ThreadLocal详解 - 夏末秋涼 - 博客园 (cnblogs.com)
JUC并发编程
wait 和 sleep 方法的区别
|
wait() |
sleep() |
所属对象 |
Object类 |
Thread类 |
使用范围 |
同步方法、同步代码块 |
没有限制 |
是否会释放锁 |
会 |
不会 |
使用场景 |
线程间的交互、通信 |
暂停执行 |
实际开发中的多线程操作方式
JDK中的Executors虽然提供了如newFixedThreadPool()
、newSingleThreadExecutor()
、newCachedThreadPool()
等创建线程池的方法,但都有其局限性,不够灵活。
使用ThreadPoolExecutor有助于大家明确线程池的运行规则,创建符合自己的业务场景需要的线程池,避免资源耗尽的风险。
java线程池ThreadPoolExecutor类使用详解 - DaFanJoy - 博客园 (cnblogs.com)
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory)
|
CountDownLatch
countDownLatch是一个同步工具类,用来协调多个线程之间的同步,或者说起到线程之间的通信(而不是用作互斥的作用)。
CountDownLatch能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。使用一个计数器进行实现。计数器初始值为线程的数量。当每一个线程完成自己任务后,计数器的值就会减一。当计数器的值为0时,表示所有的线程都已经完成一些任务,然后在CountDownLatch上等待的线程就可以恢复执行接下来的任务。
用法一
一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。
- 某一线程在开始运行前等待n个线程执行完毕。将CountDownLatch的计数器初始化为new CountDownLatch(n),每当一个任务线程执行完毕,就将计数器减1:
countdownLatch.countDown()
- 当计数器的值变为0时,在CountDownLatch上
await()
的线程就会被唤醒。
用法二
实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。
初始化一个共享的CountDownLatch(1),将其计算器初始化为1,多个线程在开始执行任务前首先countdownlatch.await(),当主线程调用countDown()时,计数器变为0,多个线程同时被唤醒。
CountDownLatch的理解和使用 - Shane_Li - 博客园 (cnblogs.com)