Java多线程
Process与Thread
- 程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。
- 而进程则是执行程序的一次执行过程,它是一个动态的概念。是系统资源分配的单位(程序的最小单位,是资源调度的最小单位)
- 通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程是CPU调度和执行的的单位。
- 注意:很多多线程是模拟出来的,真正的多线程是指有多个cpu,即多核,如服务器。如果是模拟出来的多线程,即在一个cpu的情况下,在同一个时间点,cpu只能执行一个代码,因为切换的很快,所以就有同时执行的错局。
核心概念
- 线程就是独立的执行路径;
- 在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,gc线程;
- main()称之为主线程,为系统的入口,用于执行整个程序;
- 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能认为的干预的。
- 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制;
- 线程会带来额外的开销,如cpu调度时间,并发控制开销。
- 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致
线程创建
- 三种创建方式
- Thread class(继承Thread类(重点) 启动线程:子类对象.start(),不建议使用:避免OOP单继承局限性)
- Runnable接口(实现Runnable接口(重点) 启动线程:传入目标对象+Thread对象.start() 推荐使用:避免单继承局限性,灵活方便,方便同一个对象被多个线程使用)(Thread(对象名,“线程名称”))
- Callable接口(实现Callable接口(了解))
方式一:
//创建线程方式一:继承Thread类,重写run()方法,调用start开启线程
//总结:注意,线程开启不一定立即执行,由cPU调度执行
public class TestThread1 extends Thread{
public void run(){
//run方法线程体
for (int i = 0; i < 200; i++) {
system.out.println("我在看代码--—""+i) ;
}
}
public static void main(string[] args){
// main线程,主线程
// 创建一个线对象
TestThread1 testThread1 = new TestThread1( ;
//调用start()方法开启线程
testThreadi.start(); for (int i = 0; i < 1000; i++) {
system.out.println("我在学习多线程-—""+i) ;
}
}
}//交替出现
练习Thread,实现多线程同步下载图片
package Test;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
public class TestThread extends Thread
{
private String url;
private String name;
public TestThread(String url,String name) {
this.name=name;
this.url=url;
}
public void run() {
Webdown we=new Webdown();
we.downloader(url,name);
System.out.println("下载了文件:"+name);
}
public static void main(String[] args) {
TestThread t1=new TestThread("https://p1.ssl.qhimgs1.com/sdr/400__/t01fff3e327dfefd757.jpg","小兔子.jpg");
TestThread t2=new TestThread("https://p.ssl.qhimg.com/sdm/420_207_/t016d0fa3c28f6468b7.jpg","樱花.jpg");
t1.start();
t2.start();
}
}
class Webdown {
public void downloader(String url,String name){
try {
FileUtils.copyURLToFile(new URL(url),new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("问题");
}
}
}
方式2:
//创建线程方式2:实现runnable接口,重写run方法,执行线程需要丢入runnable接口实现类,调用start方法.
public class TestThread3 implements Runnablet{ public void run(){
//run方法线程体
for (int i=e; i< 200; i++){
System.out.printIn("我在看代码---"+i);
public static void main(String[] args){
//创建runnbale接口的实现类对象
TestThread3 testThread3 = new TestThread3():
//创建线程对象,通过线程对象来开启我们的线程,代理
// thread thread = new Thread(testThread3);
// thread.start(): 1
new Thread(testThread3).start();
for (int i=g: i s 1000; i++){
System.out.printIn("我在学习多线程-"+i);}
多个线程操作同一个对象
- 多个线程操作同一个资源的精况下,线程不安全,数据紊乱。
//多个线程同时操作同一个对象
//买火车票的例子
//发现问题;多个线程操作同一个资源的精况下,线程不安全,数据紊乱。
public class TestThread4 implements Runnable {
private int ticketNums = 10;
public void run(){
while (true) {
if (ticketNums<=0){
break;
}
//模拟延时
try{
Thread.sleep(millis: 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 , name:"小明").start();
new Thread(ticket, name:"老师").start();
new Thread(ticket , name:"黄牛党").start();
}
}
龟兔赛跑
//龟兔赛跑 实例
/**
* 1 自定义一个boolean方法 决出胜利者
* 2 将逻辑代码放入 线程体run方法中
* 3 开启乌龟、兔子两条线程
*/
public class game {
public static void main(String[] args) {
Race race = new Race();
new Thread(race, "兔子").start();
new Thread(race, "乌龟").start();
}
}
class Race implements Runnable {
public int steps = 10000;
public String winner;
@Override
public void run() {
while (steps > 0) {
if (Thread.currentThread().getName().equals("兔子")) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
boolean flag = gameover(steps);
if (flag) {
break;
} else {
//没到终点就继续跑
System.out.println(Thread.currentThread().getName() + "跑了" + steps-- + "步");
}
}
}
//决出胜利者=
public boolean gameover(int step) {
if (winner != null) {
//已经产生了胜利者
return true;
}
if (step <= 1) {
winner = Thread.currentThread().getName();
System.out.println(winner + "赢了");
return true;
}
return false;
}
}
实现Callable(了解)
- 1.实现Callable接口,需要返回值类型
- 2.重写call方法,需要抛出异常
- 3.创建目标对象
- 4.创建执行服务:ExecutorService ser = Executors.newFixedThreadPool(1);
- 5.提交执行:Future result1 = ser.submit(t1);
- 6.获取结果: boolean r1 = result1.get()
- 7.关闭服务: ser.shutdownNow();
//多线程下载图片
//实现图片的下载要先导入common-io的jar包
public class TestCallable {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//执行线程
downThread t1 = new downThread("https://img-bss.csdnimg.cn/1615183593661.png","a.jpg");
downThread t2= new downThread("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fa0.att.hudong.com%2F30%2F29%2F01300000201438121627296084016.jpg&refer=http%3A%2F%2Fa0.att.hudong.com&app=2002&size=f9999," +
"10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1618125226&t=206ef43a6621700711f384e286ee4220","b.jpg");
//创建执行服务 开启线程池
ExecutorService ser= Executors.newFixedThreadPool(2);
//提交执行
Future<Boolean> future1=ser.submit(t1);
Future<Boolean> future2=ser.submit(t2);
//获取结果
boolean flag1= future1.get();
boolean flag2= future2.get();
System.out.println(flag1);
System.out.println(flag2);
//关闭服务
ser.shutdownNow();
}
}
class downThread implements Callable<Boolean> {
public String url;
public String name;
public downThread(String url,String name){
this.url=url;
this.name=name;
}
@Override
public Boolean call(){
//执行体
webDownLoad webDownLoad = new webDownLoad();
webDownLoad.photoDown(url,name);
System.out.println(name+"图片下载完成");
return true;
}
}
class webDownLoad {
//定义下载图片的方法
public void photoDown(String url, String name) {
//借助封装的类 FileUtils 将该路径的图片 拷贝到指定文件
String file="G:\\idea\\bilibili\\kuang\\Thread\\src\\com\\zz\\demo01";
try {
//将网络路径 url 的资源下载到指定的文件夹中去,并命名为name
FileUtils.copyURLToFile(new URL(url), new File(file,name));
} catch (IOException e) {
e.printStackTrace();
}
}
}
静态代理
//静态代理 多线程也是静态代理原理实现
public class Proxy {
public static void main(String[] args) {
//真实角色
you you = new you();
//代理角色
Marry marry = new WeddingCompany(you);
//代理角色 帮助真实角色实现
marry.marride();
}
}
interface Marry{
//定义一个结婚接口
void marride();
}
//真实角色 结婚
class you implements Marry{
@Override
public void marride() {
System.out.println("xxx结婚了");
}
}
//婚庆公司 帮助我实现结婚 婚庆公司可以在结婚前后增加所需要的业务
class WeddingCompany implements Marry{
//引入真实角色
public Marry target;
public WeddingCompany(Marry target){
this.target=target;
}
@Override
public void marride() {
before();
target.marride();
after();
}
private void before() {
System.out.println("结婚前,进行场地的布置");
}
private void after() {
System.out.println("结婚后,收拾东西回公司");
}
}
结果:
结婚前
xxx结婚了
结婚后
静态代理模式总结:
-
真实对象和代理对象都要实现同一个接口
-
代理对象要代理真是角色
-
好处
- 代理对象可以做很多真实对象做不了的事
- 真实对象专注做自己的事情
-
Thread底层也是通过静态代理原理实现的,Thread为代理角色,Runnable为真实角色,run为接口中的方法
线程方法
方法 | 说明 |
---|---|
setPriority(int newPriority) | 更改线程的优先级 |
static void sleep(long millis) | 在指定的毫秒内让正在执行的线程进入休眠状态 |
void join() | 等待该线程终止 |
static void yield() | 暂停正在执行的线程对象,并执行其他的线程 |
void interrupt() | 中断线程,别使用这个方法 |
boolean isAlive() | 测试线程是否处于活动状态 |
停止线程
- 建议线程正常停止—>利用次数,不建议死循环。
- 建议使用标志位—>设置—个标志位
- 不要使用stop或者destroy等过时或者JDK不建议使用的方法
public class TestStop implements Runnable{
boolean flag=true;
@Override
public void run() {
int i=0;
while (flag){
System.out.println("子线程"+i++);
}
}
//标志位停止线程
private 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("主线程"+i);
if (i==900){
testStop.stop();
System.out.println("线程停止。");
}
}
}
}
线程休眠
- sleep (时间) 指定当前线程阻塞的毫秒数;
- sleep存在异常InterruptedException;
- sleep时间达到后线程进入就绪状态;
- sleep可以模拟网络延时(放大问题的发生性),倒计时等。
- 每一个对象都有一个锁,sleep不会释放锁;
//模拟网络延时 放大问题的发生性
public class TestSleep implements Runnable {
//模拟卖票
public int tickets=50;
@Override
public void run() {
while (tickets>0){
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"买到了第"+tickets--+"张票");
}
}
public static void main(String[] args) {
TestSleep testSleep = new TestSleep();
new Thread(testSleep,"小明").start();
new Thread(testSleep,"小王").start();
new Thread(testSleep,"老张").start();
}
}
//模拟倒计时
package Theraddd;
public class TimeLeft {
public static void main(String[] args) throws InterruptedException {
TimeLeft.dd();
}
public static void dd() throws InterruptedException {
int n=10;
while(true){
Thread.sleep(1000);
System.out.println(n--);
if(n<=0){
break;
}
}
}
}
线程礼让
- 将线程从运行状态转为就绪状态
- 让cpu重新调度,礼让不一定成功!看CPU心情
- Thread.yield();
join
- Join合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞
- 两个线程交替执行,当使用join()方法时,主线程停止等到子线程完成才继续执行。
public class TheardJoin implements Runnable{
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("子线程"+i);
}
}
public static void main(String[] args) {
TheardJoin testJoin = new TheardJoin();
Thread thread = new Thread(testJoin);
thread.start();
for (int i = 0; i < 500; i++) {
//当我们主线程i==50时,让子线程进行一个插队
if (i==50){
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("主线程"+i);
}
}
}
线程状态
-
NEW
线程尚未开始在这个国家。RUNNABLE
处于这种状态中的java虚拟机执行的线程。BLOCKED
线程阻塞等待监控锁在这个国家。WAITING
处于这种状态的线程被无限期地等待另一个线程来执行特定的动作。TIMED_WAITING
处于这种状态的线程正在等待另一个线程上执行一个动作指定的等待时间。TERMINATED
处于这种状态的线程退出。
一个线程可以在一个给定的时间点上只有一个状态。这些状态是不反映任何操作系统线程状态的虚拟机状态。
public class TestThreadState {
public static void main(String[] args) {
Thread thread=new Thread(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
System.out.println("子线程");
});
//创建完对象后,观察线程的状态
Thread.State state = thread.getState();
System.out.println(state);
//start后线程进入就绪状态
thread.start();
System.out.println(thread.getState());
while (state!=Thread.State.TERMINATED){
//若线程不死 就一直输出
//等待状态说明执行中,出现了阻塞睡眠
try {
Thread.sleep(100);
state=thread.getState();
System.out.println(state);
} catch (InterruptedException e) {
}
}
//线程停止之后不能再次运行,会出现异常
//Exception in thread "main" java.lang.IllegalThreadStateException
//at java.lang.Thread.start(Thread.java:708)
//at com.zz.state.TestThreadState.main(TestThreadState.java:40)
//thread.start();
}
}
输出结果:
NEW
RUNNABLE
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
子线程
TERMINATED
线程优先级
- Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行
- 线程的优先级用数字表示,范围从1~10.
- Thread.MIN_PRIORITY = 1;
- Thread.MAX_PRIORITY = 10;
- Thread.NORM_PRIORITY = 5;
- 使用以下方式改变或获取优先级
- getPriority() . setPriority(int xxx)
- 线程优先级最大设置为10,最小为1,不在这个范围内的都会报出异常
- 每一个程序都是主线程先执行,后执行子线程,默认线程优先级为5
- 优先级低只是意味着获得调度的概率低,并不是优先级低就不会被调用了,这都是看CPU的调度,也有可能低优先级的线程先调度(性能倒置)
public class TestPriority implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"----->"+Thread.currentThread().getPriority());
}
public static void main(String[] args) {
System.out.println("主线程优先级"+Thread.currentThread().getPriority());
TestPriority testPriority = new TestPriority();
Thread thread1=new Thread(testPriority,"线程1");
thread1.setPriority(1);
thread1.start();
Thread thread2=new Thread(testPriority,"线程2");
thread2.setPriority(Thread.MAX_PRIORITY);//10
thread2.start();
Thread thread3=new Thread(testPriority,"线程3");
thread3.setPriority(3);
thread3.start();
}
}
守护(daemon)线程
- 线程分为用户线程和守护线程
- 虚拟机必须确保用户线程执行完毕
- 虚拟机不用等待守护线程执行完毕 如gc线程
- 如,后台记录操作日志,监控内存,垃圾回收等待…
- 设置为守护线程核心就是将线程的setDaemon(true)设置为true
- 当用户线程结束后,我们看到守护线程还在执行一段时间,原因是当我们销毁用户线程后也需要一段时间,因为cpu切换速度比较快,所以我们的守护线程也会运行一段时间。
//守护线程
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("上帝一直保佑着你");
}
}
}
//用户线程
class You implements Runnable{
@Override
public void run() {
for (int i = 0; i < 36500; i++) {
System.out.println("hello world 第"+i+"天");
}
System.out.println("GoodBy world");
}
}
线程同步
并发和并行
并发是两个队列交替使用,并行是两个队列同时使用两台咖啡机,如果串行,一个队列使用一台咖啡机,这效率最低的。
并发(多线程操作同一个资源)
- CPU一核,模拟出来多条线程,天下武功,唯快不破,快速交替
并行(多个人一起行走)
- CPU多核,多个线程可以同时执行;提高性能可以用线程池
并发编程的本质:充分利用CPU性能
线程同步
-
处理多线程问题时 , 多个线程访问同一个对象 , 并且某些线程还想修改这个对象 . 这时候我们就需要线程同步 . 线程同步其实就是一种等待机制 , 多个需要同时访问此对象的线程进入这个对象的等待池 形成队列, 等待前面线程使用完毕 , 下一个线程再使用
-
由于同一进程的多个线程共享同一块存储空间 , 在带来方便的同时,也带来了访问冲突问题 , 为了保证数据在方法中被访问时的正确性 , 在访问时加入 锁机制
synchronized , 当一个线程获得对象的排它锁 , 独占资源 , 其他线程必须等待 , 使用后释放锁即可 . 存在以下问题 :
- 一个线程持有锁会导致其他所有需要此锁的线程挂起 ;
- 在多线程竞争下 , 加锁 , 释放锁会导致比较多的上下文切换 和 调度延时,引起性能问题 ;
- 如果一个优先级高的线程等待一个优先级低的线程释放锁 会导致优先级倒置 , 引起性能问题
同步方法
- 由于我们可以通过 private 关键字来保证数据对象只能被方法访问 , 所以我们只需要针对方法提出一套机制 , 这套机制就是 synchronized 关键字 , 它包括两种用法 synchronized 方法 和synchronized 块
- synchronized 方法 是我们共享的资源通过一个方法执行 同步方法 : public synchronized void method(int args) {}
- synchronized 块 是我们共享的资源是一个对象 块 synchronized (accout){ }
- synchronized方法控制对 “对象” 的访问 , 每个对象对应一把锁 , 每个synchronized方法都必须获得调用该方法的对象的锁才能执行 , 否则线程会阻塞 , 方法一旦执行 , 就独占该锁 , 直到该方法返回才释放锁 , 后面被阻塞的线程才能获得这个锁 , 继续执行
缺陷 : 若将一个大的方法申明为synchronized 将会影响效率(方法里需要修改的资源才需要锁,锁的太多浪费资源)
同步块
- 同步块 : synchronized (Obj ) { }
- Obj 称之为 同步监视器
- Obj 可以是任何对象 , 但是推荐使用共享资源作为同步监视器
- 同步方法中无需指定同步监视器 , 因为同步方法的同步监视器就是this , 就是这个对象本身 , 或者是 class [ 反射中讲解 ]
- 同步监视器的执行过程
- 第一个线程访问 , 锁定同步监视器 , 执行其中代码 .
- 第二个线程访问 , 发现同步监视器被锁定 , 无法访问 .
- 第一个线程访问完毕 , 解锁同步监视器 .
- 第二个线程访问, 发现同步监视器没有锁 , 然后锁定并访问
线程同步安全
买票
这里多个线程资源抢占票,若不适用锁机制不同对象会抢到同一张票,还会造成票的负数,显然这是线程不安全的,那么我们使用synchronized方法实现线程安全
//多个对象进行抢票 通过给票上锁来实现线程安全
public class Ticket {
public static void main(String[] args) {
MyTicket myTicket = new MyTicket();
new Thread(myTicket).start();
new Thread(myTicket).start();
new Thread(myTicket).start();
}
}
class MyTicket implements Runnable {
public int tickets = 20;
boolean flag=true;
@Override
public void run() {
while (true) {
buy();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//synchronized 同步方法,锁的是 this MyTicket这个资源
public synchronized void buy() {
if (tickets<0){
flag=false;
return;
}
System.out.println(Thread.currentThread().getName() + "抢到了" + tickets--);
}
}
取钱(ACID补充)
两个线程进行取钱,取同一个账户,若不加锁两个线程会同时操控一个资源进行,会造成当a取钱时,b也同时进行取钱,就会造成ab同时消耗这个资源,会造成数据的脏读。使用代码块,锁住变化的对象账户accout
/**
* 模拟夫妻取钱,开启两个线程,同时取钱,一个账户
*/
public class Bannk {
public static void main(String[] args) {
Accout accout = new Accout(100,"卖废品");
MyThread myThread1 = new MyThread(accout,50);
myThread1.start();
MyThread myThread2 = new MyThread(accout,50);
myThread2.start();
}
}
class Accout{
//银行卡余额
public int money;
//存钱备注
public String name;
public Accout(int money, String name) {
this.money = money;
this.name = name;
}
}
class MyThread extends Thread{
public Accout accout;
//取的钱
public int quMoney;
//手里的钱
public int nowMoney;
public MyThread(Accout accout, int quMoney) {
this.accout = accout;
this.quMoney = quMoney;
}
boolean flag=true;
@Override
public void run() {
//锁的对象时变化的对象,这里若使用同步方法,指代this代表银行,明显不行
//使用代码块,锁住变化的对象账户accout
synchronized (accout){
//判断取的钱 是否多余账户余额
if (quMoney>accout.money){
flag=false;
System.out.println(Thread.currentThread().getName()+"取了"+quMoney+"银行卡余额不足,仅剩"+accout.money);
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
//正常取钱
accout.money=accout.money-quMoney;
nowMoney+=quMoney;
System.out.println(Thread.currentThread().getName()+"取了"+quMoney+",手里:"+nowMoney+",银行卡余额:"+accout.money);
}
}
}
ArrayList
不安全原因是当两个线程同时执行的时候,将两个元素加到了同一个数组太快了后面会覆盖前面的元素,就会造成集合的长度小于10000,
Vector 是线程安全的
public class unSafeList {
public static void main(String[] args) {
ArrayList<Object> list = new ArrayList<>();
//Vector<Object> objects = new Vector<>();
//不安全原因是当两个线程同时执行的时候,将两个元素加到了同一个数组
for (int i = 0; i < 10000; i++) {
new Thread(()->{
//这里变化的资源是list所以锁住该对象
synchronized (list){
list.add(Thread.currentThread().getName());
}
}).start();
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("集合中大小:"+list.size());
}
}
CopyOnWriteArrayList 线程安全
CopyOnWriteArrayList 是JUC下的一个方法,在java.util.concurrent.CopyOnWriteArrayList这个包下面
//线程安全
public class TestConyOnWriteArraylist {
public static void main(String[] args) {
CopyOnWriteArrayList<Object> list = new CopyOnWriteArrayList<>();
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());
}
}
源码分析 CopyOnWriteArrayList为什么是线程安全的
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private static final long serialVersionUID = 8673264195747942595L;
/** The lock protecting all mutators */
//这里使用了可重复锁,是JUC里面的锁,后面会讲到
final transient ReentrantLock lock = new ReentrantLock();
/** The array, accessed only via getArray/setArray. */
//使用了volatile关键字 保证了原子性
private transient volatile Object[] array;
/**
* Gets the array. Non-private so as to also be accessible
* from CopyOnWriteArraySet class.
*/
final Object[] getArray() {
return array;
}
死锁
- 多个线程各自占有一些共享资源 , 并且互相等待其他线程占有的资源才能运行 , 而导致两个或者多个线程都在等待对方释放资源 , 都停止执行的情形 . 某一个同步块 同时拥有 “ 两个以上对象的锁 ” 时 , 就可能会发生 “ 死锁 ” 的问题 .
代码示例 :
A B两个线程 a,b 两个资源 A首先拥有a资源,B首先拥有b资源;两个线程各自争夺对方手中的资源,产生死锁
public class DeadLock {
public static void main(String[] args) {
DeadThread thread1= new DeadThread(0,"A");
thread1.start();
DeadThread thread2= new DeadThread(1,"B");
thread2.start();
}
}
//两个资源
//a
class Gun{
String name="a";
}
//b
class Doll {
String name = "b;
}
class DeadThread extends Thread{
static Gun gun=new Gun();
static Doll doll=new Doll();
int choice;
String name;
public DeadThread(int choice,String name){
this.choice=choice;
this.name=name;
}
@Override
public void run() {
makeup();
}
public void makeup(){
if (choice==0){
synchronized (gun){
System.out.println(Thread.currentThread().getName()+"获得了----》"+gun.name+"的锁");
//当走在这里的时候,小王也拿到了doll的锁并没有释放,
synchronized (doll){
System.out.println(Thread.currentThread().getName()+"获得了----》"+gun.name+"的锁");
}
}
}else {
synchronized (doll){
System.out.println(Thread.currentThread().getName()+"获得了----》"+doll.name+"的锁");
//这里小明一开始就拿到了 gun的锁并没有释放,故就产生了死锁
synchronized (gun){
System.out.println(Thread.currentThread().getName()+"获得了----》"+gun.name+"的锁");
}
}
}
}
}
解决死锁
当小王想要抢小明手中枪的时候,必须等待对方释放锁后,才能拿得到,不在产生死锁
package Theraddd;
public class DeadLock {
public static void main(String[] args) {
DeadThread thread1= new DeadThread(0,"A");
thread1.start();
DeadThread thread2= new DeadThread(1,"B");
thread2.start();
}
}
//两个资源
//a
class Gun{
String name="a";
}
//b
class Doll {
String name = "b";
}
class DeadThread extends Thread{
static Gun gun=new Gun();
static Doll doll=new Doll();
int choice;
String name;
public DeadThread(int choice,String name){
this.choice=choice;
this.name=name;
}
@Override
public void run() {
makeup();
}
public void makeup(){
if (choice==0){
synchronized (gun){
System.out.println(Thread.currentThread().getName()+"获得了----》"+gun.name+"的锁");
//当走在这里的时候,小王也拿到了doll的锁并没有释放,
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (doll){
System.out.println(Thread.currentThread().getName()+"获得了----》"+doll.name+"的锁");
}
}else {
synchronized (doll){
System.out.println(Thread.currentThread().getName()+"获得了----》"+doll.name+"的锁");
//这里小明一开始就拿到了 gun的锁并没有释放,故就产生了死锁
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (gun){
System.out.println(Thread.currentThread().getName()+"获得了----》"+gun.name+"的锁");
}
}
}
}
死锁避免
产生死锁的四个必要条件:
- 互斥条件:一个资源每次只能被一个进程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件 : 进程已获得的资源,在末使用完之前,不能强行剥夺。
- 循环等待条件 : 若干进程之间形成一种头尾相接的循环等待资源关系。
上面列出了死锁的四个必要条件,我们只要想办法破其中的任意一个或多个条件就可以避免死锁发生
Lock(锁)
- 从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当
- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
- ReentrantLock (可重入锁)类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁
public class TestLock {
public static void main(String[] args) {
LockThread lockThread = new LockThread();
new Thread(lockThread).start();
new Thread(lockThread).start();
new Thread(lockThread).start();
}
}
class LockThread implements Runnable {
private int tickets = 10;
ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
lock.lock();
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "---->" + tickets--);
Thread.sleep(1000);
} else {
break;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
synchronized 与 Lock 的对比
- Lock是显式锁(手动开启和关闭锁,别忘记关闭锁)synchronized是隐式锁,出了作用域自动释放
- Lock只有代码块锁,synchronized有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
- 优先使用顺序:
- Lock > 同步代码块(已经进入了方法体,分配了相应资源)> 同步方法(在方法体之外)
线程协作
线程通信
- 应用场景 : 生产者和消费者问题
- 假设仓库中只能存放一件产品 , 生产者将生产出来的产品放入仓库 , 消费者将仓库中产品取走消费 .
- 如果仓库中没有产品 , 则生产者将产品放入仓库 , 否则停止生产并等待 , 直到仓库中的产品被消费者取走为止 .
- 如果仓库中放有产品 , 则消费者可以将产品取走消费 , 否则停止消费并等待 ,直到仓库中再次放入产品为止。
分析:
这是一个线程同步问题 , 生产者和消费者共享同一个资源 , 并且生产者和消费者之 间相互依赖 , 互为条件 .
- 对于生产者 , 没有生产产品之前 , 要通知消费者等待 . 而生产了产品之后 , 又需要马上通知消费者消费
- 对于消费者 , 在消费之后 , 要通知生产者已经结束消费 , 需要生产新的产品以供消费.
- 产者消费者问题中 , 仅有synchronized是不够的
- Snchronized 可阻止并发更新同一个共享资源 , 实现了同步
- Snchronized 不能用来实现不同线程之间的消息传递 (通信)
Java多线程学习之wait、notify/notifyAll 详解
1、wait()、notify/notifyAll() 方法是Object的本地final方法,无法被重写。
2、wait()使当前线程阻塞,前提是 必须先获得锁,一般配合synchronized 关键字使用,即,一般在synchronized 同步代码块里使用 wait()、notify/notifyAll() 方法。
3、 由于 wait()、notify/notifyAll() 在synchronized 代码块执行,说明当前线程一定是获取了锁的。
当线程执行wait()方法时候,会释放当前的锁,然后让出CPU,进入等待状态。
只有当 notify/notifyAll() 被执行时候,才会唤醒一个或多个正处于等待状态的线程,然后继续往下执行,直到执行完synchronized 代码块的代码或是中途遇到wait() ,再次释放锁。
也就是说,notify/notifyAll() 的执行只是唤醒沉睡的线程,而不会立即释放锁,锁的释放要看代码块的具体执行情况。所以在编程中,尽量在使用了notify/notifyAll() 后立即退出临界区,以唤醒其他线程让其获得锁
4、wait() 需要被try catch包围,以便发生异常中断也可以使wait等待的线程唤醒。
5、notify 和wait 的顺序不能错,如果A线程先执行notify方法,B线程在执行wait方法,那么B线程是无法被唤醒的。
6、notify 和 notifyAll的区别
notify方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。notifyAll 会唤醒所有等待(对象的)线程,尽管哪一个线程将会第一个处理取决于操作系统的实现。如果当前情况下有多个线程需要被唤醒,推荐使用notifyAll 方法。比如在生产者-消费者里面的使用,每次都需要唤醒所有的消费者或是生产者,以判断程序是否可以继续往下执行。
7、在多线程中要测试某个条件的变化,使用if 还是while?
要注意,notify唤醒沉睡的线程后线程会接着上次的执行继续往下执行。所以在进行条件判断时候,可以先把 wait 语句忽略不计来进行考虑;显然,要确保程序一定要执行,并且要保证程序直到满足一定的条件再执行,要使用while进行等待,直到满足条件才继续往下执行。
解决方法
1.管程法
并发协作模型 “ 生产者 / 消费者模式 ” —->管程法
生产者 : 负责生产数据的模块 (可能是方法 , 对象 , 线程 , 进程) ;
消费者 : 负责处理数据的模块 (可能是方法 , 对象 , 线程 , 进程) ;
缓冲区 : 消费者不能直接使用生产者的数据 , 他们之间有个 “ 缓冲区
生产者将生产好的数据放入缓冲区 , 消费者从缓冲区拿出数据
public class TestShoppig {
public static void main(String[] args) {
SyContainer syContainer = new SyContainer();
new Product(syContainer).start();
new Consumer(syContainer).start();
}
}
//定义两个线程 生产者 消费者
class Product extends Thread {
SyContainer syContainer;
public Product(SyContainer syContainer){
this.syContainer=syContainer;
}
@Override
public void run() {
super.run();
for (int i = 1; i < 100; i++) {
syContainer.push(new Chicken(i));
System.out.println("生产了--->" + i);
}
}
}
class Consumer extends Thread {
SyContainer syContainer;
public Consumer(SyContainer syContainer){
this.syContainer=syContainer;
}
@Override
public void run() {
super.run();
for (int i = 1; i < 100; i++) {
Chicken pop = syContainer.pop();
System.out.println("消费了"+pop.count);
}
}
}
//鸡
class Chicken {
//数量
int count;
public Chicken(int id) {
this.count = id;
}
}
//盘子
class SyContainer {
int count = 0;
//一个盘子放10只鸡
Chicken[] chickens = new Chicken[10];
//生产者 放入产品
public synchronized void push(Chicken chicken) {
if (count == chickens.length) {
//盘子已满 通知消费者消费,生产者等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
//未满 生产丢入盘子
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 TestTv {
public static void main(String[] args) {
Tv tv = new Tv();
new Thread(new Player(tv)).start();
new Thread(new Watcher(tv)).start();
}
}
class Tv {
//演员表演 观众观看
private 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.flag=!this.flag;
this.voice=voice;
}
public synchronized void watch(){
if (flag){
try {
//等待演出
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("观看了--->"+voice);
this.notifyAll();
this.flag=!this.flag;
}
}
//演出者
class Player implements Runnable{
Tv tv;
public Player(Tv tv) {
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
if (i%2==0){
tv.play("光头强");
}
tv.play("喜洋洋");
}
}
}
//观众
class Watcher implements Runnable{
Tv tv;
public Watcher(Tv tv) {
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
tv.watch();
}
}
}
线程池法
- 背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
- 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。
- 可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
- 好处:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理(….)
- corePoolSize:核心池的大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没有任务时最多保持多长时间后会终止
- JDK 5.0起提供了线程池相关API:ExecutorService 和 Executors
- ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
- void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行Runnable
- Future submit(Callable task):执行任务,有返回值,一般又来执行Callable
- void shutdown() :关闭连接池
- Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
public class TestPool {
public static void main(String[] args) {
//创建线程池 后面参数为线程池大小
ExecutorService executorService = Executors.newFixedThreadPool(5);
executorService.execute(new ThreadPool());
executorService.execute(new ThreadPool());
executorService.execute(new ThreadPool());
executorService.execute(new ThreadPool());
executorService.execute(new ThreadPool());
//关闭连接
executorService.shutdown();
}
}
class ThreadPool implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}