java线程(Java.Thread)
一、线程简介
1.多任务
- 现实中太多这样同时做多件事情的例子了,看起来是多个任务都在做,其实本质上我们的大脑在同一时间依旧只做了一件事情。
2.多线程
- 原来是一条路,慢慢因为车太多了,道路阻塞,效率极低。为了提高使用的效率,能够充分利用道路,于是加了多个车道。
3.普通方法调用和多线程
4.Process与Thread
-
程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。
-
而进程则是执行程序的依次执行过程,它是一个动态的概念。是系统资源分配的单位。
-
通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程是CPU调度和执行的单位。
注意:
很多多线程是模拟出来的,真正的多线程是指有多个cpu,即多核,如服务器。如果是模拟出来的多线程,即在一个cpu的情况下,在同一个时间点,cpu只能执行一个代码,因为切换的很快,所以就有同时执行的错局。
5.总结
- 线程就是独立的执行路径
- 在程序运行时,即使没有自己创建线程,后台也会有多个线程,比如主线程,GC线程
- main()称之为主线程,为系统的入口,用于执行整个程序
- 在一个进程中,如果开辟了多个线程,线程的运行是由调度器(cpu)安排调度的,调度器是与操作系统紧密相关的,先后顺序是不能人为干预的
- 对同一份资源操作时mm会存在资源抢夺的问题,需要加入并发控制
- 线程会带来额外的开销,如CPU调度时间,并发控制开销
- 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致
二、线程实现
1.三种创建方式
1.1 继承Thread类
-
自定义线程类继承Thread类
-
重写**run()**方法,编写线程执行体
-
创建线程对象,调用**start()**方法启动线程
//创建线程方式一:继承Thread类,重写run()方法,调用start开启线程
public class TestThread1 extends Thread {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("我在执行run方法-->" + i);
SleepUtil.sleep(100);
}
}
public static void main(String[] args) {
TestThread1 thread = new TestThread1();
thread.start();
for (int i = 0; i < 20; i++) {
System.out.println("我在执行main方法-->" + i);
SleepUtil.sleep(100);
}
}
}
- 运行结果
1.2 实现Runable接口
-
自定义线程类实现Runnable接口
-
实现**run()**方法,编写线程执行体
-
创建线程对象,调用**start()**方法启动对象
//创建线程方式二:实现runnable接口
public class TestThread2 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("run方法正在执行-->" + i);
SleepUtil.sleep(10);
}
}
public static void main(String[] args) {
TestThread2 thread2 = new TestThread2();
new Thread(thread2).start();
for (int i = 0; i < 20; i++) {
System.out.println("main方法正在执行-->" + i);
SleepUtil.sleep(10);
}
}
}
- 运行结果
1.3 案例一:买火车票
public class TestThread3 implements Runnable {
private int ticketNumber = 10;
@Override
public void run() {
while (ticketNumber >= 0) {
System.out.println(Thread.currentThread().getName() + "拿到了第" + ticketNumber-- + "张票");
SleepUtil.sleep(2);
}
}
public static void main(String[] args) {
TestThread3 thread3 = new TestThread3();
new Thread(thread3, "段金宇").start();
new Thread(thread3, "孙宜辰").start();
new Thread(thread3, "黄牛").start();
}
}
- 运行结果
问题:多个线程抢夺资源会出现资源重复使用
1.4 案例二:龟兔赛跑
//案例二:龟兔赛跑
public class TestThread4 implements Runnable {
private String winner;
@Override
public void run() {
int length = 100;
while (length > 0) {
if (Thread.currentThread().getName().equals("兔子")) {
//模拟兔子中间休息
if (length == 50) {
SleepUtil.sleep(300);
}
//兔子每次跑两米
length = length - 2;
} else {
//乌龟每次跑一米
length = length - 1;
}
SleepUtil.sleep(5);
//判断比赛是否结束
if (isOver(length)) {
break;
}
System.out.println(Thread.currentThread().getName() + "距离终点还有-->" + length + "米");
}
}
public boolean isOver(int len) {
if (winner != null) return true;
if (len <= 0) {
winner = Thread.currentThread().getName();
System.out.println("比赛结束,胜利者是" + Thread.currentThread().getName());
return true;
}
return false;
}
public static void main(String[] args) {
TestThread4 thread4 = new TestThread4();
new Thread(thread4, "兔子").start();
new Thread(thread4, "乌龟").start();
}
}
- 运行结果
1.5 实现Callable接口(了解即可)
-
实现Callable接口,需要返回值类型
-
重写call方法,需要抛出异常
-
创建目标对象
-
创建执行服务:ExecutorService ser = Executors.newFixedThreadPool(1);
-
提交执行:Future result1 = ser.submit(11);
-
获取结果:boolean r1 = result1.get()
-
关闭服务:ser.shutdownNow();
public class TestThread5 implements Callable {
@Override
public Boolean call() throws Exception {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+"正在执行-->" + i);
}
return true;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
TestThread5 thread1 = new TestThread5();
TestThread5 thread2 = new TestThread5();
//创建执行服务
ExecutorService pool = Executors.newFixedThreadPool(3);
//提交执行
Future<Boolean> t1 = pool.submit(thread1);
Future<Boolean> t2 = pool.submit(thread1);
//获得结果
Boolean res1 = t1.get();
Boolean res2 = t2.get();
if (res1 == true) System.out.println("线程一完成任务");
if (res2 == true) System.out.println("线程二完成任务");
//关闭服务
pool.shutdown();
}
}
执行结果:
优点:
- 可以定义返回值
- 可以抛出异常
1.6 总结
继承Thread类 | 实现Runnable接口 |
---|---|
子类继承Thread类具备多线程能力 | 实现接口RUnnable具有多线程能力 |
子类对象.start() | 传入目标对象+Thread对象.start() |
不建议使用:避免OOP单继承局限性 | 推荐使用:便面单继承局限性、灵活方便,同一个对象可被多个线程使用 |
2.代理模式
2.1 静态代理
- 你:真实角色
- 婚庆公司:代理你,帮你处理结婚事宜
- 结婚:你和婚庆公司都要做的事情
//结婚接口
public interface TestProxy1 {
void happyMarry();
}
//结婚主人公:被代理者
class You implements TestProxy1 {
@Override
public void happyMarry() {
System.out.println("段金宇结婚当日......");
}
}
//婚庆公司:代理者
class WeddingCompany implements TestProxy1 {
private TestProxy1 target;
public WeddingCompany(TestProxy1 target) {
this.target = target;
}
@Override
public void happyMarry() {
before();
target.happyMarry();
after();
}
private void before() {
System.out.println("布置婚礼现场");
}
private void after() {
System.out.println("收分子钱");
}
public static void main(String[] args) {
WeddingCompany weddingCompany = new WeddingCompany(new You());
weddingCompany.happyMarry();
}
}
- 运行结果
- 总结:
- 真是对象和代理对象都要实现同一个接口
- 代理对象要代理真是角色
- 代理对象可以做真实角色无法做的事情
- 真实角色可以专注于做自己的事情
2.2 lambda表达式
- λ:希腊字母表中第11位,英语名称为Lambda
- 避免匿名内部类定义过多
- 实质属于函数式编程
实现方法:
new Thread(() -> System.out.println("这是lambda表达式" + Thread.currentThread().getName())).start();
- 为什么要是用Lambda表达式
- 避免匿名内部类定义过多
- 可以让代码看起来更简洁
- 去掉没有意义的代码。只留下核心逻辑
- 函数接口定义
- 任何接口,如果只包含一个抽象方法,那他就是一个函数式接口
- 对于函数式接口可以使用Lambda表达式
public class TestThread2 implements Runnable{
@Override
public void run() {
}
}
}
2.3 Lambda表达式的演化过程
-
外部类
public class TestProxy3 { public static void main(String[] args) { Like like = new ILike(); like.lambda(); } } interface Like{ void lambda(); } class ILike implements Like{ @Override public void lambda() { System.out.println("i like"); } }
-
静态内部类
public class TestProxy4 { public static class ILike implements Like{ @Override public void lambda() { System.out.println("i Like"); } } public static void main(String[] args) { ILike iLike = new ILike(); iLike.lambda(); } }
-
局部内部类
public class TestProxy5 {
public static void main(String[] args) {
ILike iLike = new ILike();
iLike.lambda();
class ILike implements Like {
@Override
public void lambda() {
System.out.println("i Like");
}
}
}
}
- 匿名内部类(没有类的名称,必须借助接口或父类)
public class TestProxy5 {
public static void main(String[] args) {
Like like = new Like() {
@Override
public void lambda() {
System.out.println("i like");
}
};
like.lambda();
}
}
-
Lambda表达式
public class TestProxy6 { public static void main(String[] args) { Like like1 = () -> System.out.println("i like"); like1.lambda(); } }
三、线程状态
1.线程状态
2.线程方法
方法 | 说明 |
---|---|
void setPriority(int newPriority) | 更改线程的优先级 |
void sleep(long millis) | 指定线程休眠毫秒数 |
void join() | 等待该线程终止 |
void yield() | 暂停当前正在执行的线程对象,并执行其他线程 |
void interrupt() | 中断线程(不建议使用) |
boolean isAlive() | 测试线程是否处于活动状态 |
3.停止线程stop()
-
不推荐使用JDK提供的stop()、destory()方法。【已废弃】
-
推荐线程自己停下来
- 设置标志位进行终止变量
- 设定循环/执行次数
public class TestStatus1 implements Runnable { //定义执行标识 private boolean flag = true; @Override public void run() { //在线程体中使用该标识 while (flag) { System.out.println("run ... Thread"); } } //对外提供方法改变标识 public void stop() { this.flag = false; } }
4.线程休眠sleep()
- sleep(时间)指定当前线程阻塞的毫秒数;
- sleep存在异常InterruptedException;
- sleep时间达到后线程进入就绪状态;
- sleep可以模拟网络延时,倒计时等。
- 每一个对象都有一个锁sleep不会释放锁;
案例:每秒打印当前系统时间
public class TestSleep implements Runnable {
@Override
public void run() {
while (true) {
Date date = new Date(System.currentTimeMillis());
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
TestSleep testSleep = new TestSleep();
new Thread(testSleep).start();
}
}
5.线程礼让yield()
- 礼让线程,让当前正在执行的线程暂停,但不阻塞
- 将线程从运行状态转为就绪状态
- 让cpu重新调度,礼让不一定成功
public class TestYield implements Runnable {
@Override
public void run() {
System.out.println("线程" + Thread.currentThread().getName() + ":start....");
Thread.yield();
System.out.println("线程" + Thread.currentThread().getName() + ":end....");
}
public static void main(String[] args) {
TestYield testYield = new TestYield();
new Thread(testYield, "a").start();
new Thread(testYield, "b").start();
}
}
运行结果
- 礼让成功
- 礼让失败
6.线程插队join()
- join合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞
public class TestJoin implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("vip线程正在执行...." + i);
SleepUtil.sleep(100);
}
}
public static void main(String[] args) throws InterruptedException {
TestJoin testJoin = new TestJoin();
Thread thread = new Thread(testJoin);
thread.start();
for (int i = 0; i < 10; i++) {
System.out.println("普通线程在执行...." + i);
SleepUtil.sleep(100);
if (i == 5) {
thread.join();
SleepUtil.sleep(50);
}
}
}
}
从第六次循环开始,vip线程插队执行,直到执行完普通线程继续执行
7.观测线程状态
-
NEW
尚未启动的线程处于此状态。
-
RUNNABLE
在Java虚拟机中执行的线程处于此状态。 -
BLOCKED
被阻塞等待监视器锁定的线程处于此状态。 -
WAITING
正在等待另一个线程执行特定动作的线程处于此状态。 -
TIMED_WAITING
正在等待另一个线程执行动作达到指定等待时间的线程处于此状态。 -
TERMINATED
已退出的线程处于此状态。
public class TestWatchStatus {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (int i = 0; i < 2; i++) {
SleepUtil.sleep(100);
System.out.println("子线程");
}
System.out.println("/");
});
System.out.println(thread.getState());
thread.start();
System.out.println(thread.getState());
SleepUtil.sleep(100);
while (thread.getState().equals(Thread.State.TIMED_WAITING)) {
SleepUtil.sleep(100);
System.out.println(thread.getState());
}
SleepUtil.sleep(1000);
System.out.println(thread.getState());
}
}
观察结果:
8.线程优先级
-
Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。
-
线程的优先级用数字表示,范围从1~10。
/** * 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;
-
改变优先级的方法
thread.setPriority();
public class TestPriority implements Runnable {
public static void main(String[] args) {
TestPriority testPriority = new TestPriority();
Thread thread = new Thread(testPriority);
thread.setPriority(Thread.MAX_PRIORITY);
thread.start();
thread = new Thread(testPriority);
thread.setPriority(Thread.MIN_PRIORITY);
thread.start();
thread = new Thread(testPriority);
thread.setPriority(4);
thread.start();
thread = new Thread(testPriority);
thread.setPriority(7);
thread.start();
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "--正在执行" + "其优先级为:" + Thread.currentThread().getPriority());
}
}
优先级数值越大越先执行
9.守护线程(daemon)
-
线程分为用户线程和守护线程
-
虚拟机必须确保用户线程执行完毕
-
虚拟机不用等待守护线程执行完毕
eg:后台记录操作日志,监控内存,垃圾回收等待…
public class TestDaemon {
public static void main(String[] args) {
new Thread(new You()).start();
Thread thread = new Thread(new China());
//设置为守护线程,默认为false
thread.setDaemon(true);
thread.start();
}
}
class You implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("你在快乐的活着");
SleepUtil.sleep(50);
}
}
}
class China implements Runnable {
@Override
public void run() {
while (true) {
System.out.println("国家在默默守护者你");
SleepUtil.sleep(50);
}
}
}
虚拟机不会等待守护线程结束
四、线程同步
1.并发
并发:同一个对象被多个线程同时操作
上万人同时抢票
两个银行同时取钱
2.队列和锁
线程同步的形成条件:队列+锁
3.线程同步
- 现实生活中,我们会遇到同一个资源,多个人都想使用的问题,比如,食堂排队打饭,每个人都想吃饭。最天然的解决办法就是——排队。
- 处理多线程问题时,多个线程访问同一个对象。并且某些线程还想修改这个对象。这时候我们就需要线程同步,线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程再使用。
- 由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题。为了保证数据在方法中被访问时的正确性,在访问时加入锁机制synchronized,当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可。存在以下问题:
- 一个线程持有锁会导致其他所有需要此锁的线程挂起;
- 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题;
- 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题。
4.不安全案例
4.1 买票案例
public class demo1_buyTicket {
public static void main(String[] args) {
BuyTicket buyTicket = new BuyTicket();
new Thread(buyTicket, "段金宇").start();
new Thread(buyTicket, "孙宜辰").start();
new Thread(buyTicket, "黄牛党").start();
}
}
class BuyTicket implements Runnable {
private Integer ticketNumber = 10;
@Override
public void run() {
while (true) {
boolean b = buy();
if (!b) {
break;
}
SleepUtil.sleep(1000);
}
}
private synchronized boolean buy() {
if (ticketNumber > 0) {
System.out.println(Thread.currentThread().getName() + "买到了第" + ticketNumber + "张票");
ticketNumber--;
return true;
}
return false;
}
}
未同步上锁时的结果
- 票数出现了-1
- 同一张票被多次购买
4.2 取钱案例
public class demo2_withdraw {
public static void main(String[] args) {
Bank bank = new Bank();
new Thread(bank, "段金宇").start();
new Thread(bank, "孙宜辰").start();
}
}
class Bank implements Runnable {
private int money = 100;
@Override
public void run() {
withdraw(60);
}
private void withdraw(int rest) {
if (money > rest) {
System.out.println(Thread.currentThread().getName() + "取了60万元");
money = money - rest;
System.out.println("账户剩余:" + money + "万元");
} else {
System.out.println("余额不足,去签失败");
}
}
}
5.线程同步的实现
5.1 synchronized方法
由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制synchronized,当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可。
存在以下问题︰
- 一个线程持有锁会导致其他所有需要此锁的线程挂起;
- 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题;
- 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题。
同步方法:
- 由于我们可以通过private关键字来保证数据对象只能被方法访问,所以我们只需要针对方法提出一套机制,这套机制就是synchronized关键字,它包括两种用法:synchronized方法和synchronized 块。
- synchronized方法控制对**“对象**”的访问,每个对象对应一把锁,每个
synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞。方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。
缺陷:若将一个大方法声明为synchronized将会影响效率
- 取钱案例优化
private synchronized void withdraw(int rest) {
if (money > rest) {
System.out.println(Thread.currentThread().getName() + "取了60万元");
money = money - rest;
System.out.println("账户剩余:" + money + "万元");
} else {
System.out.println("余额不足,取钱失败");
}
}
- 买票案例优化
private synchronized boolean buy() {
if (ticketNumber > 0) {
System.out.println(Thread.currentThread().getName() + "买到了第" + ticketNumber + "张票");
ticketNumber--;
return true;
}
return false;
}
5.2 同步块
-
同步块:synchronized (Obj) {}
-
Obj称之为同步监视器
- Obj可以是任何对象,但是推荐使用共享资源作为同步监视器
- 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,或者是class
-
同步监视器的执行过程
1.第一个线程访问,锁定同步监视器,执行其中代码;2.第二个线程访问,发现同步监视器被锁定,无法访问;
3.第一个线程访问完毕,解锁同步监视器;
4.第二个线程访问,发现同步监视器没有锁,然后锁定并访问。 -
买票案例优化
private boolean buy() {
synchronized (ticketNumber) {
if (ticketNumber > 0) {
System.out.println(Thread.currentThread().getName() + "买到了第" + ticketNumber + "张票");
ticketNumber--;
return true;
}
}
return false;
}
- 取钱代码块优化
private void withdraw(int rest) {
synchronized (money){
if (money > rest) {
System.out.println(Thread.currentThread().getName() + "取了60万元");
money = money - rest;
System.out.println("账户剩余:" + money + "万元");
} else {
System.out.println("余额不足,取钱失败");
}
}
}
6.线程安全的List
- 不安全的ArrayList()
List<String> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
new Thread(() -> {
list.add(Thread.currentThread().getName());
}).start();
}
SleepUtil.sleep(3000);
System.out.println(list.size());
- 加锁使其安全
List<String> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
new Thread(() -> {
synchronized (list){
list.add(Thread.currentThread().getName());
}
}).start();
}
SleepUtil.sleep(3000);
System.out.println(list.size());
- 线程安全的
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 1000; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
SleepUtil.sleep(100);
System.out.println(list.size());
7.死锁
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。某一个同步块同时拥有**“两个以上对象的锁”**时,就可能会发生“死锁”的问题。
- 死锁案例
public class TestDeadEmbrace {
public static void main(String[] args) {
Makeup lady1 = new Makeup(0, "孙宜辰");
Makeup lady2 = new Makeup(1, "孙二辰");
lady1.start();
lady2.start();
}
}
class Lipstick {
}
class Mirror {
}
class Makeup extends Thread {
static Lipstick lipstick = new Lipstick();
static Mirror mirror = new Mirror();
int choose;
String name;
public Makeup(int choose, String name) {
this.choose = choose;
this.name = name;
}
@Override
public void run() {
makeup();
}
private void makeup() {
if (choose == 0) {
synchronized (lipstick) {
System.out.println(this.name + "拿到了" + Lipstick.class.getName());
SleepUtil.sleep(100);
synchronized (mirror) {
System.out.println(this.name + "拿到了" + Mirror.class.getName());
}
}
} else {
synchronized (mirror) {
System.out.println(this.name + "拿到了" + Mirror.class.getName());
SleepUtil.sleep(200);
synchronized (lipstick) {
System.out.println(this.name + "拿到了" + Lipstick.class.getName());
}
}
}
}
}
7.1 死锁产生原因
-
系统资源的竞争
通常系统中拥有的不可剥夺资源,其数量不足以满足多个进程运行的需要,使得进程在 运行过程中,会因争夺资源而陷入僵局,如磁带机、打印机等。只有对不可剥夺资源的竞争 才可能产生死锁,对可剥夺资源的竞争是不会引起死锁的。
-
进程推进顺序非法
进程在运行过程中,请求和释放资源的顺序不当,也同样会导致死锁。例如,并发进程 P1、P2分别保持了资源R1、R2,而进程P1申请资源R2,进程P2申请资源R1时,两者都 会因为所需资源被占用而阻塞。
7.2 死锁产生的必要条件
- 互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某 资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
- 不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。
- 请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
- 循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被 链中下一个进程所请求。即存在一个处于等待状态的进程集合{Pl, P2, …, pn},其中Pi等 待的资源被P(i+1)占有(i=0, 1, …, n-1),Pn等待的资源被P0占有。
产生死锁必须同时满足以上四个条件,只要其中任一条件不成立,死锁就不会发生。
8.Lock锁
- 从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当
- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
- ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
8.1 优化购票案例
public class TestLock {
public static void main(String[] args) {
Station station = new Station(10);
new Thread(station, "段金宇").start();
new Thread(station, "孙宜辰").start();
new Thread(station, "黄牛" +
"").start();
}
}
class Station implements Runnable {
private int ticketNumber;
private boolean flag = true;
public Station(int ticketNumber) {
this.ticketNumber = ticketNumber;
}
@Override
public void run() {
while (flag) {
flag = buyTicket();
}
}
private boolean buyTicket() {
Lock lock = new ReentrantLock();
try {
lock.lock();
if (ticketNumber > 0) {
System.out.println(Thread.currentThread().getName() + "取走了第" + ticketNumber-- + "张票");
SleepUtil.sleep(100);
return true;
}
} finally {
lock.unlock();
}
return false;
}
}
lock()方法要写在try中。
9 总结
synchroinzed与lock对比
synchronized | lock |
---|---|
显示锁(手动开启和关闭锁) | 隐式锁(出了作用域自动释放) |
锁代码块 | 既可以锁代码块,也可以锁方法 |
非公平锁(无法保证线程按照锁的顺序获得锁,效率较高) | 可配置 |
不可中断 | 可中断 |
读写,读读,写写均互斥 | ReadWriteLock使读读可以并发执行 |
都是可重入锁 |
可重入锁:持有该锁的资源时可再次对改资源上锁,避免死锁
原理:通过为每个锁关联一个请求计数器和一个获得该锁的线程。当计数器为0时,认为锁是未被占用的。线程请求一个未被占用的锁时,JVM将记录该线程并将请求计数器设置为1,此时该线程就获得了锁,当该线程再次请求这个锁,计数器将递增,当线程退出同步方法或者同步代码块时,计数器将递减,当计数器为0时,线程就释放了该对象,其他线程才能获取该锁。
五、线程通信问题
1.线程通信
应用场景:生产者消费者模式
- 假设仓库中只能存放一件产品﹐生产者将生产出来的产品放入仓库﹐消费者将仓库中产品取走消费.
- 如果仓库中没有产品﹐则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止.
- 如果仓库中放有产品﹐则消费者可以将产品取走消费﹐否则停止消费并等待﹐直到仓库中再次放入产品为止.
2.线程通信方法
方法名 | 作用 |
---|---|
wait() | 线程会一直等待,直到其他线程通知。与sleep不同,线程会释放锁 |
wait(long timeout) | 指定等待的毫秒数 |
notify() | 唤醒一个语出等待状态的线程 |
notiftAll() | 唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先调度 |
3.线程通信—分析
实质是线程同步问题,生产者与消费者共享同一资源,并且之间相互依赖,互为条件。
- 对于生产者,没有生产产品之前,要通知消费者等待,而生产了产品之后,又需要马上通知消费者消费。
- 对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费。
- 在生产者消费者问题中,仅有synchronized是不够的
- synchronized可以实现并发更新资源实现了同步
- 但不能用来实现线程通信
4.线程通信问题解决方法
4.1 管程法
- 生产者:负责生产数据的模块(可能是方法、对象、线程、进程);
- 消费者:负责处理数据的模块(可能是方法、对象、线程、进程);
- 缓冲区:消费者不能直接使用生产者的数据,他们之间有个”缓冲区“
生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据
public class TestMonitor {
public static void main(String[] args) {
SynContainer container = new SynContainer();
Consumer consumer = new Consumer(container);
Producer producer = new Producer(container);
new Thread(producer, "1号").start();
new Thread(producer, "2号").start();
new Thread(consumer, "段金宇").start();
new Thread(consumer, "孙宜辰").start();
}
}
class Produce {
private String id;
public Produce(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
class SynContainer {
//容器的大小
Produce[] produces = new Produce[10];
//产品数量
int count = 0;
public synchronized void push(Produce produce) throws InterruptedException {
while (count >= produces.length - 1) this.wait();
produces[count] = produce;
count++;
//通知所有消费者消费
notifyAll();
}
public synchronized Produce pop() throws InterruptedException {
while (count <= 0) this.wait();
count--;
Produce produce = produces[count];
notifyAll();
return produce;
}
}
class Producer implements Runnable {
private SynContainer container;
public Producer(SynContainer container) {
this.container = container;
}
@Override
public void run() {
produce();
}
public void produce() {
while (true) {
String id = UUID.randomUUID().toString().replace("-", "").substring(0, 5);
Produce produce = new Produce(id);
try {
container.push(produce);
System.out.println("生产者" + Thread.currentThread().getName() + "生产了" + id + "号产品");
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Consumer implements Runnable {
private SynContainer container;
public Consumer(SynContainer container) {
this.container = container;
}
@Override
public void run() {
while (true) {
try {
Produce produce = container.pop();
System.out.println("消费者" + Thread.currentThread().getName() + "消费了" + produce.getId() + "号产品");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
4.2 信号灯法
设置标志位flag表明当前消费者或生产者谁可以行动。
六、线程池
1.使用线程池
- 背景:经常创建和销毁,使用量特比北大的资源,如并发情况下的线程,对性能影响很大。
- 思路:提前创建好多个线程、放入多个线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似于生活中的交通工具。
- 好处
- 调高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中的线程,不需要每次都创建)
- 便于线程管理
- 参数
- corePoolSize:核心池的大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没有任务时最多保持多长时间后终止
- JDK5.0起提供了线程池相关APl:ExecutorService和ExecutorsExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
- void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
- Future submit(Callable task):执行任务,有返回值,一般又来执行Callable
- void shutdown():关闭连接池
- Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
public class TestThreadPool {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(10);
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.shutdown();
}
}
class MyThread implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "正在运行");
}
}