Java—多线程详解详解详解笔记
一、线程简介
先来看一下什么是进程
进程一个进程是一个独立(self contained)的运行环境,它可以被看作一个程序或者一个应用,在一个程序中包含多个进程。JAVA运行环境中作为一个进程包含不同的类和程序流程。进程可以包含多个线程。
再来看一下什么是线程
线程可以被叫做轻量级的进程,线程需要更少的资源来创建和存在在这个过程中,线程共享进程的资源。 每个java应用程序有至少一个线程-(main方法主线程),虽然在后台有很多其它java 线程运行例如内存管理,系统管理和单进程等,但是对于应用程序来说-主线程是第一个Java线程并且我们在其中可以创建多线程。多线程是指两个或多个线程同时执行在一个程序中,在单核处理器的计算机中一次只能执行一个线程,操作系统中存在一个“时间切片” 特性用于切分处理器时间在不同的进程和线程中。
Java线程的优势
- Java线程是轻量级的相比进程来说,当创建一个线程是可以花费少的时间和资源
- 线程共享它们父进程的数据和代码。
- 上下文切换中通常线程比进程代价更少。
- 线程的内部通讯中是相对容易的比进程通讯。
二、线程实现(重点)
线程的三种创建方式
1、继承Thread类(重点)
第一步:自定义一个类继承Thread类
第二步:重写run()方法,编写线程执行体
第三步:创建线程对象,调用start()方法启动线程。
//线程创建方式一:1、继承Thread类
public class TestThread extends Thread{
//2、重写run方法
@Override
public void run() {
//run方法线程体
for (int i = 0; i < 10; i++) {
System.out.println("------run方法"+i);
}
}
public static void main(String[] args) {
//main主线程
//3、创建线程对象,调用start方法
TestThread testThread = new TestThread();
testThread.start();
//注意:在这里 调用run方法时程序就是按照先执行run 在执行main 直接调用run方法就是普通的方法调用 运行时符合顺序结构
//在这里调用的是start方法的话结果会不同,这两个是交替输出的 这里则是开启了线程,不一定立即执行 主线程和子线程是交替执行的,由cup调度
for (int i = 0; i < 20; i++) {
System.out.println("****************main方法"+i);
}
}
}
import lombok.SneakyThrows;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
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;
}
@SneakyThrows
@Override
public void run() {
Downloader downloader = new Downloader();
downloader.downloader(url,name);
System.out.println("下载了文件名为:"+name);
}
public static void main(String[] args) {
TestThread2 t1 = new TestThread2("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1600853378832&di=1ee6be0d8658070429480b1c8996babb&imgtype=0&src=http%3A%2F%2Fa0.att.hudong.com%2F70%2F91%2F01300000261284122542917592865.jpg","1.jpg");
TestThread2 t2 = new TestThread2("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1600853422032&di=5c82c4c2e6c9bcbae3b44663f16fbf40&imgtype=0&src=http%3A%2F%2Fattachments.gfan.com%2Fforum%2Fattachments2%2Fday_110320%2F11032021067b907d3ed754dd93.jpg","2.jpg");
TestThread2 t3 = new TestThread2("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1600853422032&di=b7d69eae47096c689ea17ded53bb43e5&imgtype=0&src=http%3A%2F%2Fa4.att.hudong.com%2F22%2F59%2F19300001325156131228593878903.jpg","3.jpg");
t1.start();
t2.start();
t3.start();
//这里启动虽然是按顺序启动,但是执行结果打印,每次不一定谁先下载完
//记住:线程是同步进行的!
}
}
class Downloader {
public void downloader(String url,String name ) throws IOException {
FileUtils.copyURLToFile(new URL(url),new File(name));
}
}
2、实现Runnable接口(重点的重点)
第一步:自定义一个类实现Runnable接口
第二步:重写run()方法,编写线程执行体
第三步:创建线程对象,将对象在创建Thread时作为参数传递,调用start()方法启动线程。
//创建线程2:1、使用实现Runnable方式
public class TestThread3 implements Runnable{
//2、重写run方法
@Override
public void run() {
//run方法线程体
for (int i = 0; i < 10; i++) {
System.out.println("------run方法"+i);
}
}
public static void main(String[] args) {
//main主线程
//3、创建线程对象,将对象在创建Thread时作为参数传递,调用start方法
TestThread testThread = new TestThread();
new Thread(testThread).start();
for (int i = 0; i < 20; i++) {
System.out.println("****************main方法"+i);
}
}
}
3、实现Callable接口
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.*;
/**步骤:
* 1、实现Callable接口
* 2、重写call方法,抛出异常
* 3、创建目标对象
* 4、创建执行服务:ExecutorService service = Executors.newFixedThreadPool(1);
* 5、提交执行:Future<Boolean> future = service.submit(t1);
* 6、获取结果: boolean r1 = future.get();
* 7、关闭服务: service.shutdownNow();
*/
public class TestCall implements Callable<Boolean> {
private String url;
private String name;
public TestCall(String url, String name) {
this.url = url;
this.name = name;
}
@Override
public Boolean call() throws Exception {
Downloaders downloader = new Downloaders();
downloader.downloader(url, name);
System.out.println("下载了文件名为:" + name);
return true;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
TestCall t1 = new TestCall("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1600853378832&di=1ee6be0d8658070429480b1c8996babb&imgtype=0&src=http%3A%2F%2Fa0.att.hudong.com%2F70%2F91%2F01300000261284122542917592865.jpg", "1.jpg");
TestCall t2 = new TestCall("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1600853422032&di=5c82c4c2e6c9bcbae3b44663f16fbf40&imgtype=0&src=http%3A%2F%2Fattachments.gfan.com%2Fforum%2Fattachments2%2Fday_110320%2F11032021067b907d3ed754dd93.jpg", "2.jpg");
TestCall t3 = new TestCall("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1600853422032&di=b7d69eae47096c689ea17ded53bb43e5&imgtype=0&src=http%3A%2F%2Fa4.att.hudong.com%2F22%2F59%2F19300001325156131228593878903.jpg", "3.jpg");
//创建执行服务:创建池子
ExecutorService service = Executors.newFixedThreadPool(3);
//提交执行:
Future<Boolean> future1 = service.submit(t1);
Future<Boolean> future2 = service.submit(t2);
Future<Boolean> future3 = service.submit(t3);
//获取结果:
boolean r1 = future1.get();
boolean r2 = future2.get();
boolean r3 = future3.get();
//关闭服务:
service.shutdownNow();
}
}
class Downloaders {
public void downloader(String url, String name) throws IOException {
FileUtils.copyURLToFile(new URL(url), new File(name));
}
}
4、小结
1、继承Thread类:子线程继承Thread具备了主线程能力;启动线程方式:子类对象.start() ;不建议使用这种方式创建线程:避免oop的单继承局限性;
2、实现Runnable接口:通过实现Runnable接口具备了主线程能力;启动线程方式:new Thread(传入的目标对象).start();推荐使用:接口形式灵活方便,方便同一个对象被多个线程使用!
//多线程模拟抢票
//一个对象被多个线程调用
public class TestThread4 implements Runnable {
private int ticketNums = 10;
@Override
public void run() {
while (true) {
if (ticketNums <= 0) {
break;
}
try {
//模拟延迟 200毫秒
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//Thread.currentThread().getName()获得线程名字
System.out.println(Thread.currentThread().getName() + "->拿到了第:" + ticketNums-- + "张票");
}
}
public static void main(String[] args) {
TestThread4 t= new TestThread4();
new Thread(t,"线程1").start();
new Thread(t,"线程2").start();
new Thread(t,"线程3").start();
}
}
//运行结果: 这样会有一个问题就是,线程不安全问题,不同线程可能会拿到重复的资源,一会在填坑
/**
线程2->拿到了第:9张票
线程1->拿到了第:10张票
线程3->拿到了第:10张票
线程1->拿到了第:8张票
线程2->拿到了第:8张票
线程3->拿到了第:8张票
线程3->拿到了第:7张票
线程1->拿到了第:5张票
线程2->拿到了第:6张票
线程3->拿到了第:4张票
线程2->拿到了第:2张票
线程1->拿到了第:3张票
线程2->拿到了第:1张票
线程1->拿到了第:1张票
线程3->拿到了第:1张票
*/
//龟兔赛跑 ,兔子睡觉
public class TestThread5 implements Runnable {
//胜利者
public static String winner;
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
if (Thread.currentThread().getName().equals("兔子") && i % 50 == 0) {
try {
//兔子睡觉
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
boolean flag = go(i);
if (flag) {
break;
}
System.out.println(Thread.currentThread().getName() + "->跑了" + i + "步");
}
}
private boolean go(int steps) {
//判断是否有胜利者
if (winner != null) {
return true;
}
{
if (steps >= 100) {
winner = Thread.currentThread().getName();
System.out.println("--->" + winner + "胜利了");
return true;
}
}
return false;
}
public static void main(String[] args) {
TestThread5 t5 = new TestThread5();
new Thread(t5, "乌龟").start();
new Thread(t5, "兔子").start();
}
}
5、扩展静态代理
/**
* 静态代理总结:
* 真实对象和代理对象要实现统一接口
* 代理对象要代理真是对象
*
* 代理对象可以做好多真是对象做不来的事
* 真是对象就专注做自己的事情
* */
public class StaticProxy {
public static void main(String[] args) {
new Thread(()->System.out.println("我爱你")).start();
new Wedding(new You()).HappyMarry();
}
}
//定义一个结婚接口
interface Marry {
public void HappyMarry();
}
//定义真实对象
class You implements Marry {
@Override
public void HappyMarry() {
System.out.println("要结婚了好开心,我自己专注结婚这件事");
}
}
//定义静态代理对象 (婚庆公司)
class Wedding implements Marry {
//代理谁-》真是对象
private Marry target;
public Wedding(Marry target) {
this.target = target;
}
@Override
public void HappyMarry() {
before();
this.target.HappyMarry();
after();
}
private void after() {
System.out.println("结婚后,收费");
}
private void before() {
System.out.println("结婚后,布置现场");
}
}
6、扩展Lamda表达式
Lamda表达式,也可称为闭包,它是推动 Java 8 发布的最重要新特性。读作λ表达式,它实质属于函数式编程的概念;Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中)。
为什么使用:避免匿名内部类定义过多,可以让代码看起来更整洁,去掉了没有意义的代码,保留核心。
lambda 表达式的语法格式如下:
(parameters) -> expression 或 (parameters) ->{ statements; }
lambda表达式的重要特征:
-
可选类型声明: 不需要声明参数类型,编译器可以统一识别参数值。
-
可选的参数圆括号: 一个参数无需定义圆括号,但多个参数需要定义圆括号。
-
可选的大括号: 如果主体包含了一个语句,就不需要使用大括号。
-
可选的返回关键字: 如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值。
函数式接口:
任何接口,如果只包含唯一一个抽象方法,那么他就是一个函数式接口;
public interface Runnable{ public abstract void run(); }
对于函数式接口,我们可以通过lamda表达式来创建该接口的对象。
//推导Lmada表达式 演化过程 public class Lmada { //3.静态内部类 static class Like2 implements ILike { @Override public void lamda() { System.out.println("I Like Lamda2"); } } public static void main(String[] args) { ILike like = new Like(); like.lamda(); like = new Like2(); like.lamda(); //4、局部内部类 class Like3 implements ILike { @Override public void lamda() { System.out.println("I Like Lamda3"); } } like = new Like3(); like.lamda(); //5、匿名内部类,没有名字,必须借助接口或者父类 like = new ILike() { @Override public void lamda() { System.out.println("I Like Lamda4"); } }; like.lamda(); //6.用lamda简化 like= ()->{ System.out.println("I Like Lamda5"); }; like.lamda(); } } //1、定义一个函数式接口,接口中只有一个方法 interface ILike { void lamda(); } //2、定义一个实现类 class Like implements ILike { @Override public void lamda() { System.out.println("I Like Lamda"); } }
//推导Lmada表达式 带参数
public class Lmada2 {
public static void main(String[] args) {
ILove iLove = new ILove() {
@Override
public void love(int a) {
System.out.println("i->" + a);
}
};
iLove.love(1);
//简化成lmada表达式
ILove iLove2 = (int a) -> {
System.out.println("i->" + a);
};
iLove2.love(2);
//去掉参数类型
ILove love = null;
love = (a) -> {
System.out.println("i->" + a);
};
love.love(3);
//去掉括号 只有一个参数才可以
love = a -> {
System.out.println("i->" + a);
};
love.love(4);
//去掉画括号只有一行代码才可以
love = a -> System.out.println("i->" + a);
love.love(5);
//总结:
/**
* lmada表达式在方法体只有一行代码的时候可以简化成一行代码,不然的话还是需要花括号包住的代码块!
* 也可以是多参数,多参数也可以去掉参数类型,但是必须加括号
*
* */
}
interface ILove {
void love(int a);
}
三、线程状态
-
新建状态:
使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
-
就绪状态:
当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
-
运行状态:
如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
-
阻塞状态:
如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
- 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
- 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
- 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
-
死亡状态:
一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。
线程方法
下表列出了Thread类的一些重要方法:
序号 | 方法描述 |
---|---|
1 | public void start() 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。 |
2 | public void run() 如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。 |
3 | public final void setName(String name) 改变线程名称,使之与参数 name 相同。 |
4 | public final void setPriority(int priority) 更改线程的优先级。 |
5 | public final void setDaemon(boolean on) 将该线程标记为守护线程或用户线程。 |
6 | public final void join(long millisec) 等待该线程终止的时间最长为 millis 毫秒。 |
7 | public void interrupt() 中断线程。 |
8 | public final boolean isAlive() 测试线程是否处于活动状态。 |
测试线程是否处于活动状态。 上述方法是被Thread对象调用的。下面的方法是Thread类的静态方法。
序号 | 方法描述 |
---|---|
1 | public static void yield() 暂停当前正在执行的线程对象,并执行其他线程。 |
2 | public static void sleep(long millisec) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。 |
3 | public static boolean holdsLock(Object x) 当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。 |
4 | public static Thread currentThread() 返回对当前正在执行的线程对象的引用。 |
5 | public static void dumpStack() 将当前线程的堆栈跟踪打印至标准错误流。 |
关于线程的停止
不推荐使用JDK提供的stop方法、destroy方法,因为都废弃了;
建议使用一个标志位进行终止变量当flag=flase,则终止线程;
//测试停止线程
//方法1 :线程正常停止-->利用次数,不建议死循环
//方法2:建议使用标志位-->设置一个标志位
//不建议使用stop、destroy等过时的方法停止线程!
public class ThreadStop implements Runnable {
private boolean flag = true;
@Override
public void run() {
int i = 0;
while (flag) {
System.err.println("线程运行中。。。。。。。。。。。。" + i++);
}
}
public void stop() {
this.flag = false;
}
public static void main(String[] args) {
ThreadStop threadStop =new ThreadStop();
new Thread(threadStop).start();
for (int i = 0; i < 500; i++) {
System.out.println("主线程"+i);
if (i == 400) {
//转换标志位停止线程
threadStop.stop();
System.out.println("子线程停止-----------------");
}
}
}
}
关于线程的休眠 sleep()方法
-
sleep(时间)指定当前线程阻塞的毫秒数;
-
sleep存在异常InterruptedException
-
sleep时间到达后线程会进入就绪状态;
-
sleep可以模拟网络延迟、倒计时等
-
每一个对象都有一个锁,sleep不会释放锁
关于线程的礼让 yield()方法
-
礼让线程,让当前正在执行的线程暂停,但不阻塞
-
将线程从运行状态转为就绪状态
-
让cup重新调度,礼让不一定成功!需要看cpu的心情。
public class ThreadYield { 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()方法
Join合并线程,等待此线程执行完成后,再执行其他线程,其他线程阻塞;可以想象成插队
public class ThreadJoin 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 {
ThreadJoin threadJoin = new ThreadJoin();
Thread thread = new Thread(threadJoin);
thread.start();//启动线程
//主线程
for (int i = 0; i < 500; i++) {
if (i == 200) {
thread.join();//插队
}
System.out.println("main执行--->" + i);
}
}
}
关于线程的观测状态 Thread.Sate
public class ThreadState {
@SneakyThrows
public static void main(String[] args) {
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);
}
}
}
关于线程的优先级 Priority
-
Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度那个线程来执行。
-
线程的优先级有数字1~10来表示;
-
使用以下方式改变或获取优先级 getPriority() setPriority(int x)
public class ThreadPriority {
public static void main(String[] args) {
//主线程的优先级
System.out.println(Thread.currentThread().getName() + "-->" + Thread.currentThread().getPriority());
MyPriority myPriority = new MyPriority();
Thread t1 = new Thread(myPriority, "线程1");
Thread t2 = new Thread(myPriority, "线程2");
Thread t3 = new Thread(myPriority, "线程3");
Thread t4 = new Thread(myPriority, "线程4");
Thread t5 = new Thread(myPriority, "线程5");
//先设置优先级 然后在启动。
t1.start();
t2.setPriority(3);
t2.start();
t3.setPriority(Thread.MAX_PRIORITY);//MAX_PRIORITY = 10 最高优先级
t3.start();
t4.setPriority(1);
t4.start();
t5.setPriority(6);
t5.start();
}
}
class MyPriority implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + “–>” + Thread.currentThread().getPriority());
}
}
//每次运行结果会不同,就算设置了优先级也不一定先执行,只是权重高了,具体还得看cpu的心情
**关于线程的守护 Daemon**
- 线程分为用户线程(main)和 守护线程
- 虚拟机必须保证用户线程执行完毕
- 虚拟机不用等待守护线程执行完毕 比如GC 监控内存 操作日志
```java
package com.yws.demo.learn.TestThread;
/**
* @Description: TODO
* @Author: leoYan
* @Date: 2020/11/9 11:30
*/
public class TestDaemon {
public static void main(String[] args) {
God god = new God();
Yous yous = new Yous();
Thread thread = new Thread(god);
thread.setDaemon(true);//设置守护线程 默认false 表示用户线程,一般都是用户线程
thread.start();//上帝守护线程
new Thread(yous).start();//用户线程启动
}
}
class God implements Runnable {
@Override
public void run() {
while (true) {
System.out.println("上帝守护着你");
}
}
}
class Yous implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("你今年多少岁" + i);
}
System.out.println("-------goodBay,world----------");
}
}
四、线程同步(重点)
多个线程操作一个例子。并发:同一个对象被多个线程所同时操作,处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象,这个时候我们就需要线程同步。线程同步其实是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等前一个线程执行完毕,下一个线程在使用。
形成条件:对象+锁,每个对象都有一把锁;
由于同一进程的多个线程共享同一块存储空间﹐在带来方便的同时,也带来了访问冲突问题﹐为了保证数据在方法中被访问时的正确性,在访问时加入锁机制synchronized,当一个线程获得对象的排它锁﹐独占资源﹐其他线程必须等待,使用后释放锁即可.存在以下问题:
-
一个线程持有锁会导致其他所有需要此锁的线程挂起;
-
在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题;
-
如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题.
三个不同的例子
第一个:
/**
* @Description: 模拟不安全的抢票 线程不安全可能抢到第0张票 还可能出现负数
* @Author: leoYan
* @Date: 2020/11/9 15:05
*/
public class BuyTicket {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(ticket, "线程1").start();
new Thread(ticket, "线程2").start();
new Thread(ticket, "线程3").start();
}
}
class Ticket implements Runnable {
//票
private int ticNum = 10;
boolean flag = true;
@Override
public void run() {
while (flag) {
try {
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//买票
public void buy() throws InterruptedException {
if (ticNum <= 0) {
flag = false;
return;
}
//sleep可以放大问题的发生性
Thread.sleep(10);
System.out.println(Thread.currentThread().getName() + "拿到了第" + ticNum-- + "---张票");
}
}
第二种:
package com.yws.demo.learn.TestThread;
import lombok.SneakyThrows;
/**
* @Description: 模拟两个人同时去银行取钱
* @Author: leoYan
* @Date: 2020/11/9 15:21
*/
public class DrawMoney {
public static void main(String[] args) {
Account account = new Account(100, "结婚基金");
Bank bank = new Bank(account, 50, "你");
Bank gril = new Bank(account, 100, "gril");
bank.start();
gril.start();
}
}
//账户
class Account {
int balance;//余额
String name;//账户名
public Account(int balance, String name) {
this.balance = balance;
this.name = name;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
class Bank extends Thread {
Account account;//账户
int drawMoney;//取了多少钱
int nowMoney;//现在手里有多少钱
public Bank(Account account, int drawMoney, String name) {
super(name);
this.account = account;
this.drawMoney = drawMoney;
}
//取钱
@SneakyThrows
@Override
public void run() {
if (account.balance - drawMoney < 0) {
//余额-取走的钱=0
System.out.println(Thread.currentThread().getName() + "去取钱,卡里没钱了,取不了了");
return;
}
Thread.sleep(4000);
//卡内余额 = 余额-取走的钱;
account.balance = account.balance - drawMoney;
System.out.println(account.getName() + "卡内余额" + account.balance);
//现在手里有多少钱 = 现在手里有多少钱+取走的钱
nowMoney = nowMoney + drawMoney;
System.out.println(this.getName() + "现在手里有多少钱" + nowMoney);
}
}
第三种
public static void main(String[] args) {
//线程不安全的集合
List<String> list = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
list.add(Thread.currentThread().getName());
}).start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size());
}
//结果:998 所以list是不安全的,因为可能存在两个线程操作同意对象,将其覆盖了。
//线程不安全的来源是线程的内存都是单独存在的。他们一开始看到的东西是一样的。
同步方法
由于我们可以通过private关键字来保证数据对象只能被方法访问﹐所以我们只需要针对方法提出一套机制,这套机制就是synchronized关键字,它包括两种用法︰synchronized方法和synchronized块. 只有修改代码才需要同步。
同步方法: public synchronized void method(int args)
synchronized方法控制对“对象”的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行﹐否则线程会阻塞,方法一旦执行﹐就独占该锁,直到该方法返回才释放锁﹐后面被阻塞的线程才能获得这个锁,继续执行
缺陷:若将一个大的方法申明为synchronized将会影响效率
方法里面需要修改的内容才需要锁,所太多的话会影响性能。
抢票的例子在抢票方法上加synchronized
package com.yws.demo.learn.TestThread;
/**
* @Description: 模拟安全的抢票
* @Author: leoYan
* @Date: 2020/11/9 15:05
*/
public class BuyTicket {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(ticket, "线程1").start();
new Thread(ticket, "线程2").start();
new Thread(ticket, "线程3").start();
}
}
class Ticket implements Runnable {
//票
private int ticNum = 10;
boolean flag = true;
@Override
public void run() {
while (flag) {
try {
buy();
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//买票
//synchronized 同步方法,锁的对象时 this
private synchronized void buy() throws InterruptedException {
if (ticNum <= 0) {
flag = false;
return;
}
System.out.println(Thread.currentThread().getName() + "拿到了第" + ticNum-- + "---张票");
}
}
如果在取钱的那个演示中 把run方法加上synchronized那么结果还是会出错,因为 synchronized 默认锁的的this当前对象,但是在方法中还有另一个对象。
package com.yws.demo.learn.TestThread;
import lombok.SneakyThrows;
import java.util.ArrayList;
import java.util.List;
/**
* @Description: 模拟两个人同时去银行取钱
* @Author: leoYan
* @Date: 2020/11/9 15:21
*/
public class DrawMoney {
public static void main(String[] args) {
Account account = new Account(100, "结婚基金");
Bank bank = new Bank(account, 50, "你");
Bank gril = new Bank(account, 100, "gril");
bank.start();
gril.start();
}
}
//账户
class Account {
int balance;//余额
String name;//账户名
public Account(int balance, String name) {
this.balance = balance;
this.name = name;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
class Bank extends Thread {
Account account;//账户
int drawMoney;//取了多少钱
int nowMoney;//现在手里有多少钱
public Bank(Account account, int drawMoney, String name) {
super(name);
this.account = account;
this.drawMoney = drawMoney;
}
//取钱
@SneakyThrows
@Override
public void run() {
//同步代码块 锁对象一定要锁变化的量 需要增删改的
synchronized (account){
if (account.balance - drawMoney < 0) {
//余额-取走的钱=0
System.out.println(Thread.currentThread().getName() + "去取钱,卡里没钱了,取不了了");
return;
}
Thread.sleep(4000);
//卡内余额 = 余额-取走的钱;
account.balance = account.balance - drawMoney;
System.out.println(account.getName() + "卡内余额" + account.balance);
//现在手里有多少钱 = 现在手里有多少钱+取走的钱
nowMoney = nowMoney + drawMoney;
System.out.println(this.getName() + "现在手里有多少钱" + nowMoney);
}
}
}
同步块:synchronized (Obj ){ }
Obj称之为同步监视器
Obj可以是任何对象,但是推荐使用共享资源作为同步监视器
同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this ,就是这个对象本身﹐或者是class[反射中讲解]
同步监视器的执行过程
1.第一个线程访问,锁定同步监视器﹐执行其中代码.
2.第二个线程访问﹐发现同步监视器被锁定﹐无法访问.
3.第一个线程访问完毕,解锁同步监视器.
4.第二个线程访问,发现同步监视器没有锁﹐然后锁定并访问
//引申线程安全的集合 在juc包下的
public static void main(String[] args) throws InterruptedException {
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
list.add(Thread.currentThread().getName());
}).start();
}
Thread.sleep(1000);
System.out.println(list.size());
}
死锁
多个线程各自占有一些共享资源﹐并且互相等待其他线程占有的资源才能运行﹐而导致两个或者多个线程都在等待对方释放资源﹐都停止执行的情形.某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题.
package com.yws.demo.learn.TestThread;
import lombok.SneakyThrows;
/**
* @Description: 死锁 多个线程互相抱着对象的资源,形成僵持
* @Author: leoYan
* @Date: 2020/11/9 16:24
*/
public class DeadLock {
public static void main(String[] args) {
MakeUp makeUp = new MakeUp(0, "小红");
MakeUp make = new MakeUp(1, "小丽");
makeUp.start();
make.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;
}
@SneakyThrows
@Override
public void run() {
markUp();
}
//化妆 互相持有对方的锁,就是需要拿到对方的资源
private void markUp() throws InterruptedException {
if (choose == 0) {
synchronized (lipstick) {
System.out.println(this.name + "---获得口红");
Thread.sleep(1000);
synchronized (mirror) {
System.out.println(this.name + "---获得镜子");
}
}
} else {
synchronized (mirror) {
System.out.println(this.name + "---获得镜子");
Thread.sleep(1000);
synchronized (lipstick) {
System.out.println(this.name + "---获得口红");
}
}
}
}
}
怎么解决僵持:把锁岔开,先用一个锁,用完放开再用一个
package com.yws.demo.learn.TestThread;
import lombok.SneakyThrows;
/**
* @Description: 死锁 多个线程互相抱着对象的资源,形成僵持
* @Author: leoYan
* @Date: 2020/11/9 16:24
*/
public class DeadLock {
public static void main(String[] args) {
MakeUp makeUp = new MakeUp(0, "小红");
MakeUp make = new MakeUp(1, "小丽");
makeUp.start();
make.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;
}
@SneakyThrows
@Override
public void run() {
markUp();
}
//化妆 互相持有对方的锁,就是需要拿到对方的资源
private void markUp() throws InterruptedException {
if (choose == 0) {
synchronized (lipstick) {
System.out.println(this.name + "---获得口红");
Thread.sleep(1000);
}
synchronized (mirror) {
System.out.println(this.name + "---获得镜子");
}
} else {
synchronized (mirror) {
System.out.println(this.name + "---获得镜子");
Thread.sleep(1000);
}
synchronized (lipstick) {
System.out.println(this.name + "---获得口红");
}
}
}
}
避免死锁的方法
产生死锁的四个必要条件:
1.互斥条件:一个资源每次只能被一个进程使用。
2.请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
3.不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
4.循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
上面列出了死锁的四个必要条件,我们只要想办法破其中的任意一个或多个条件就可以避免死锁发生
Lock(锁)
1.从JDK5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程2.对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
3.ReentrantLock(可重入锁)类实现了Lock,它拥 有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
package com.yws.demo.learn.TestThread;
import lombok.SneakyThrows;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Description: TODO
* @Author: leoYan
* @Date: 2020/11/9 16:43
*/
public class TestLock {
public static void main(String[] args) {
RLock rLock = new RLock();
new Thread(rLock).start();
new Thread(rLock).start();
new Thread(rLock).start();
}
}
class RLock implements Runnable {
int tick = 10;
//使用可重入锁
private final ReentrantLock lock = new ReentrantLock();
@SneakyThrows
@Override
public void run() {
while (true) {
try{//加锁
lock.lock();
if (tick > 0) {
Thread.sleep(3000);
System.out.println(tick--);
}else{
break;
}
}finally {
//解锁
lock.unlock();
}
}
}
}
//----------------------------------------------------------------------
//Lock锁
class A {
private final ReentrantLock lock = new ReenTrantLock();
public void m() {
lock.lock();
try {
//保证线程安全的代码;
} finally {
lock.unlock();
// 如果同步代码有异常,要将unlock() 写入finally语句块
}
}
}
synchronized 与Lock的对比:
Lock是显式锁(手动开启和关闭锁,别忘记关闭锁) synchronized是隐式锁,出了作用域自动释放
Lock只有代码块锁,synchronized有代码块锁和方法锁
使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
◆优先使用顺序:
Lock >同步代码块(已经进入了方法体,分配了相应资源)>同步方法(在方法体之外)
五、线程通信
生产者和消费者的关系:
这是一个线程同步问题生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件.
对于生产者,没有生产产品之前,要通知消费者等待﹒而生产了产品之后,又需要马上通知消费者消费
对于消费者﹐在消费之后,要通知生产者已经结束消费﹐需要生产新的产品以供消费.
在生产者消费者问题中,仅有synchronized是不够的
synchronized可阻止并发更新同一个共享资源,实现了同步synchronized不能用来实现不同线程之间的消息传递(通信)
Java提供了几个方法解决线程之间的通信问题:
方法名 | 作用 |
---|---|
wait() | 表示线程一直等待,知道其他线程通知,与sleep不同会释放锁 |
wait(long timeout) | 指定等待时间 |
notify() | 唤醒一个处在等待状态的线程 |
notifyAll() | 唤醒同一对象上所有调用wait()方法的线程,优先级越高的线程优先调度。 |
注意:均是Object类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常lllegalMonitorStateException
方法一 管程法
并发协作模型“生产者/消费者模式”—>管程法
生产者:负责生产数据的模块(可能是方法﹐对象﹐线程﹐进程);
消费者:负责处理数据的模块(可能是方法﹐对象,线程,进程);
缓冲区:消费者不能直接使用生产者的数据﹐他们之间有个“缓冲区“生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据
package com.yws.demo.learn.TestThread;
/**
* @Description: 测试生产者和消费者之间的关系 利用数据缓冲 管程法
* @Author: leoYan
* @Date: 2020/11/10 14:30
*/
public class TestPC {
public static void main(String[] args) {
SynContainer synContainer = new SynContainer();
new Producter(synContainer).start();
new Consumption(synContainer).start();
}
}
//生产者
class Producter extends Thread {
SynContainer synContainer;
public Producter(SynContainer synContainer) {
this.synContainer = synContainer;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
synContainer.push(new Chicken(i));
System.out.println("生产者生产第:" + i + "只鸡");
}
}
}
//消费者
class Consumption extends Thread {
SynContainer synContainer;
public Consumption(SynContainer synContainer) {
this.synContainer = synContainer;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("消费者消费第:" + synContainer.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) {
//如果容器满了,通知消费
if (count == chickens.length) {
//通知消费者,生产者等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果没满,继续生产
chickens[count] = chicken;
count++;
//通知消费者消费
this.notify();
}
//消费者使用产品
public synchronized Chicken pop() {
//容器消费者等待,通知生产者生产
if (count == 0) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//消费
count--;
Chicken chicken = chickens[count];
//通知生产者生产
this.notify();
return chicken;
}
}
方法二 信号灯发 增加一个标志位
package com.yws.demo.learn.TestThread;
/**
* @Description: 测试生产和消费关系--》信号灯法 利用标志位
* @Author: leoYan
* @Date: 2020/11/10 14:51
*/
public class TestPC2 {
public static void main(String[] args) {
TV tv = new TV();
new Actor(tv).start();
new Audience(tv).start();
}
}
//演员
class Actor extends Thread {
TV tv;
public Actor(TV tv) {
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
if (i % 2 == 0) {
this.tv.show("CCTV1");
} else {
this.tv.show("奥力给");
}
}
}
}
//观众
class Audience extends Thread {
TV tv;
public Audience(TV tv) {
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
this.tv.watch();
}
}
}
//TV
class TV {
String showName;//节目名
boolean flag = true;//标志位
//表演方法
public synchronized void show(String showName) {
if (!flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("表演了节目---》" + showName);
this.showName = showName;
//通知观看
this.notifyAll();
//看完成产
this.flag = !this.flag;
}
//观看
public synchronized void watch() {
if (flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("观看了---》" + showName);
this.notifyAll();
this.flag = !this.flag;//保证状态最新
}
}
线程池
背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
好处:
提高响应速度(减少了创建新线程的时间)
降低资源消耗(重复利用线程池中线程,不需要每次都创建)
便于线程管理(…)
corePoolSize:核心池的大小
maximumPoolSize:最大线程数
keepAliveTime:线程没有任务时最多保持多长时间后会终止
JDK 5.0起提供了线程池相关API: ExecutorService和Executors
ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
void execute(Runnable command)∶执行任务/命令,没有返回值,一般用来执行Runnable
Future submit(Callable task):执行任务,有返回值,一般又来执行Callable
void shutdown()︰关闭连接池
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
package com.yws.demo.learn.TestThread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @Description: 测试线程池
* @Author: leoYan
* @Date: 2020/11/10 15:11
*/
public class TestPool {
public static void main(String[] args) {
//1 创建线程池 newFixedThreadPool 池子大小
ExecutorService executorService = Executors.newFixedThreadPool(10);
//执行
executorService.execute(new MyThread());
executorService.execute(new MyThread());
executorService.execute(new MyThread());
executorService.execute(new MyThread());
//2 关闭连接
executorService.shutdown();
}
}
class MyThread implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}