java多线程基础学习
一、线程简介1.类比2.程序进程线程3.线程的核心概念 二、线程的实现(重点)调用方法与调用多线程的区别Thread 类1.thread使用方法2. 代码实现3.应用 Runnable 接口1.Runable使用方法2. 代码实现3.应用 Callable接口1.Callable使用方法2.代码实现 三、线程的状态线程的5个状态线程方法观察线程的状态线程的优先级守护线程(daemon) 四、线程同步同步方法与同步块死锁Lock(锁) 五、线程协作生产者消费者模式线程池一、线程简介
所有的资料来源都是以【狂神说Java】多线程详解为基础
【狂神说Java】多线程详解
建议看视频,这篇博客是笔记
同时要结合目录看
1.类比
类比于现实中的的多任务,在同一个时间段,同时做几件事情
在程序中就是主线程与子线程同时进行任务
2.程序进程线程
在操作系统中的运行的程序就是进程,而进程通常采用多进程执行
程序
指令和数据的有序集合,本身没有任何运行的含义,是静态概念
进程
是执行程序的一次执行过程,是一个动态概念,是系统资源分配的单位
线程
一个进程通常包括多个线程,线程是CPU调度和执行的单位
3.线程的核心概念
二、线程的实现(重点)
调用方法与调用多线程的区别
run()方法是需要执行的方法体
start()方法是开启多线程的方法体
Thread 类
不建议使用:避免OOP单继承局限性
1.thread使用方法
自定义线程类继承Thread类重写run方法,编写线程执行体在主线程中创建线程对象,使用start方法启动线程2. 代码实现
package cn.livorth;public class TestThread01 extends Thread{@Overridepublic void run() {//在run()方法中编写执行体for (int i = 0; i < 100; i++) {System.out.println("在子线程中执行--" + i);}}public static void main(String[] args) {//在主线程中创建线程对象,使用start方法启动线程TestThread01 testThread = new TestThread01();testThread.start();for (int i = 0; i < 100; i++) {System.out.println("在主线程中执行---" + i);}}}
效果如下:
并行交替执行,轮流占用cpu
3.应用
样例来自《狂神说java-多线程详解》
代码(详解在注释):
package cn.livorth;import mons.io.FileUtils;import java.io.File;import java.io.IOException;import .URL;public class TestThread02 extends Thread{private String url;private String filename;//构造函数传入所需的url和filenamepublic TestThread02(String url, String filename) {this.url = url;this.filename = filename;}@Overridepublic void run() {//调用downLoader来获取文件webDownLoader webDownLoader = new webDownLoader();webDownLoader.downLoader(url, filename);System.out.println(filename + "下载完成");}public static void main(String[] args) {//传入三张图图片的urlTestThread02 t1 = new TestThread02("/bfs/album/" +"481ba607ba31e9932b90e383f3698fec4c1d9577.jpg@518w_1e_1c.jpg", "pic01.jpg");TestThread02 t2 = new TestThread02("/bfs/album/" +"98d71302c030aa86258eb17a5db084bfadf8ff39.jpg@518w_1e_1c.jpg", "pic02.jpg");TestThread02 t3 = new TestThread02("/bfs/album/" +"70849f611883c3e7feffc730c4f1e7b7173c9695.jpg@518w_1e_1c.jpg", "pic03.jpg");//开始多线程下载t1.start();t2.start();t3.start();}}class webDownLoader{/*** 通过url获取文件内容,并保存* @param url* @param name*/public void downLoader(String url, String name){try {//调用Commons_io包里面的方法copyURLToFileFileUtils.copyURLToFile(new URL(url), new File(name));} catch (IOException e) {e.printStackTrace();System.out.println("IO异常,downLoader出现问题");}}}
结果:
这只是一个简单的对多线程的小实践,事实上通过这样获取图片还是有诸多问题
Runnable 接口
推荐使用:避免单继承的局限性,方便同一个对象被多个进程调用
1.Runable使用方法
自定义MyRunable类实现Runable接口重写run方法,编写线程执行体在主线程中创建线程对象,使用start方法启动线程2. 代码实现
package cn.livorth;public class TestThread03 implements Runnable{@Overridepublic void run() {//在run()方法中编写执行体for (int i = 0; i < 100; i++) {System.out.println("在子线程中执行--" + i);}}public static void main(String[] args) {//创建Runable接口的实现类对象TestThread03 testThread = new TestThread03();//创建线程对象,通过线程对象来开启线程,也就是代理Thread thread = new Thread(testThread);thread.start();//也可以缩写为://new Thread(testThread).start();for (int i = 0; i < 100; i++) {System.out.println("在主线程中执行---" + i);}}}
效果如下:
在实现的效果上是没有区别的:
3.应用
按照狂神说的那个样例来的话,会出现问题
对个线程操作同一个资源的时候,线程不安全,数据会紊乱
所以不写了
Callable接口
仅作了解即可
1.Callable使用方法
通过服务提交线程
实现Callable接口,需要返回值类型重写call方法, 需要抛出异常创建目标对象创建执行服务: ExecutorService ser = Executors.newFixedThreadPool(1);提交执行: Future result1 = ser.submit(t1);获取结果: boolean r1 = result1.get()关闭服务: ser.shutdownNow();
2.代码实现
改自Thread中的应用
《狂神说java-多线程详解》-P8
package cn.livorth;import java.util.concurrent.*;public class TestCallable01 implements Callable<Boolean> {private String url;private String filename;//构造函数传入所需的url和filenamepublic TestCallable01(String url, String filename) {this.url = url;this.filename = filename;}@Overridepublic Boolean call() {//调用downLoader来获取文件webDownLoader webDownLoader = new webDownLoader();webDownLoader.downLoader(url, filename);System.out.println(filename + "下载完成");return true;}public static void main(String[] args) {//传入三张图图片的urlTestCallable01 t1 = new TestCallable01("/bfs/album/" +"481ba607ba31e9932b90e383f3698fec4c1d9577.jpg@518w_1e_1c.jpg", "pic01.jpg");TestCallable01 t2 = new TestCallable01("/bfs/album/" +"98d71302c030aa86258eb17a5db084bfadf8ff39.jpg@518w_1e_1c.jpg", "pic02.jpg");TestCallable01 t3 = new TestCallable01("/bfs/album/" +"70849f611883c3e7feffc730c4f1e7b7173c9695.jpg@518w_1e_1c.jpg", "pic03.jpg");//创建执行服务(参数为线程池的大小)ExecutorService service = Executors.newFixedThreadPool(3);//提交执行Future<Boolean> r1 = service.submit(t1);Future<Boolean> r2 = service.submit(t2);Future<Boolean> r3 = service.submit(t3);//获取结果(不写应该也是可以的,这里应该是作为一种检验参数)try {boolean rs1 = r1.get();boolean rs2 = r2.get();boolean rs3 = r3.get();System.out.println(rs1);System.out.println(rs2);System.out.println(rs3);} catch (InterruptedException | ExecutionException e) {e.printStackTrace();}//关闭服务service.shutdown();}}
结果:
三、线程的状态
线程的5个状态
创建状态:Thread t = new Thread();就绪状态:调用start方法运行状态:被cpu调度执行阻塞状态:调用sleep、wait或同步锁定时,进入阻塞状态死亡状态:线程中断或者结束,一旦死亡就不再启动线程方法
停止线程
不推荐使用JDK提供的stop()、destroy()。
推荐让线程自己停下来,但也不建议使用死循环
建议使用一个标志位作为终止变量,当flag==false时,线程终止运行
package cn.livorth.state;public class TestStop implements Runnable{private boolean flag = true;@Overridepublic void run() {int i = 0;while(flag){System.out.println("子进程执行中---" + 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 < 100; i++) {System.out.println("主进程执行中---" + i);if(i == 90){testStop.stop();System.out.println("子进程结束运行");}}}}
线程休眠
sleep()方法的单位是毫秒
sleep()存在异常InterruptedException
sleep完了线程进入就绪状态,不会直接进入执行态
可以用于模拟网络延时,倒计时等
每个对象都有一个锁,sleep不释放锁
try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}
模拟网络延时可以放大问题的发生性
线程礼让
礼让不一定成功,yield方法是当前正在执行的进程进入就绪态,等待cpu的调度
public void run() {System.out.println(Thread.currentThread().getName()+"线程开始执行");Thread.yield();//礼让System.out.println(Thread.currentThread().getName()+"线程停止执行");}
Join
join合并线程,只能是当前线程执行完之后才能执行其他线程,对其他线程造成阻塞
package cn.livorth.state;public class TestJoin implements Runnable{@Overridepublic void run() {for (int i = 0; i < 1000; i++) {System.out.println("子线程执行---" + i);}}public static void main(String[] args) {TestJoin testJoin = new TestJoin();Thread thread = new Thread(testJoin);thread.start();for (int i = 0; i < 500; i++) {if(i == 200){try {//阻塞主线程,使子线程优先执行完thread.join();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("主线程执行---" + i);}}}
观察线程的状态
代码样例:
package cn.livorth.state;public class TestState {public static void main(String[] args) {Thread thread = new Thread(()->{for (int i = 0; i < 5; i++) {try {Thread.sleep(100);} 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);//RUNABLE//阻塞时与结束时while(state != Thread.State.TERMINATED){try {Thread.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}state = thread.getState();//TIMED_WAITING或者TERMINATEDSystem.out.println(state);}}}
结果样例:
线程的优先级
Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。
线程的优先级用数字表示,范围从1~10.
Thread.MIN_ PRIORITY= 1;Thread.MAX PRIORITY = 10;Thread.NORM_ PRIORITY= 5;
使用以下方式获取或改变优先级
getPriority() ,setPriority(int xxx)
注意:
先设置优先级再启动
main方法的默认优先级为5
理论上来说优先级越高的越先执行,哪怕他start更晚
守护线程(daemon)
线程分为用户线程和守护线程虚拟机必须确保用户线程执行完毕.虚拟机不用等待守护线程执行完毕如,后台记录操作日志,监控内存,垃圾回收等待…代码示例:
package cn.livorth.state;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{@Overridepublic void run() {//上帝永生,无限循环while (true)System.out.println("上帝在你身旁");}}class You implements Runnable{@Overridepublic void run() {//人生有限,100年后撒手人寰for (int i = 0; i < 100; i++) {System.out.println("happy everyday");}System.out.println("goodbye world");}}
结果:
上帝明明是个死循环,但是进程还是结束了。
因为这里将上帝线程设置为了守护线程,虚拟机不加以考虑
四、线程同步
并发:同一个对象被多个线程同时操作,也就是不同线程同时操作同一个资源地址,造成数据紊乱
同步:多个需要同时访问资源的线程进入对象的等待池,等待前面线程使用完毕
锁:每个对象都有把锁,当获取对象时,独占资源,其他线程必须等待,使用结束后才释放
一个线程持有锁会导致其他所有需要此锁的线程挂起在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题
同步方法与同步块
synchronized关键字
synchronized默认锁的是他自身的对象,要是跨对象,通常使用同步块,即锁共享资源所在的对象
同步方法
public synchronized void method(int args){}
因为每个对象对应一把锁,使用加了关键字之后,方法一旦执行就独占该锁
缺陷:若将一个大的方法申明为synchronized将会影响效率
同步块
synchronized(Obj){}
Obj称之为同步监视器Obj可以是任何对象,但是推荐使用共享资源作为同步监视器同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this ,就是这个对象本身或者是class [反射中讲解] 同步监视器的执行过程: 第一个线程访问,锁定同步监视器,执行其中代码.第二个线程访问 ,发现同步监视器被锁定,无法访问第一个线程访问完毕 ,解锁同步监视器.第二个线程访问,发现同步监视器没有锁,然后锁定并访问
死锁
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形. 某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题.
我的理解:
张三占用着A资源,等着B资源,李四占用着B资源,等着A资源
张三同时有AB资源才能完成任务,李四也一样,但是两个人谁都不会先放手
然后就进入了死锁
产生死锁的四个必要条件:
互斥条件: 一个资源每次只能被一个进程使用请求与保持条件: 一个进程因请求资源而阻塞时,对已获得的资源保持不放不剥夺条件: 进程已获得的资源,在末使用完之前,不能强行剥夺循环等待条件: 若干进程之间形成一种头尾相接的循环等待资源关系
只要破除任意一个就能避免死锁
Lock(锁)
通过显示定义同步锁对象(Lock)来实现同步
ReentrantLock类实现了Lock接口,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
演示代码:
package cn.livorth.gaoji;import java.util.concurrent.locks.ReentrantLock;public class TestLock {public static void main(String[] args) {TestLock2 testLock = new TestLock2();new Thread(testLock, "A").start();new Thread(testLock, "B").start();new Thread(testLock, "C").start();}}class TestLock2 implements Runnable{int count = 1000;//定义lock锁private final ReentrantLock lock = new ReentrantLock();@Overridepublic void run() {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}while(true){try {//进入加锁状态lock.lock();if(count > 0)System.out.println(Thread.currentThread().getName() + "---" +count--);elsebreak;}finally {//解锁lock.unlock();}}}}
说明:
在不加锁的情况下,ABC可能会同时操作到count,导致数据紊乱
在加了锁之后,ABC排队操作
五、线程协作
生产者消费者模式
有一说一这个我们上个学期的操作系统讲的很详细了
生产者与消费者共享一个资源,同时生产者与消费者相互依赖互为条件
生产者访问仓库,往里面放;消费者也要访问,但是只拿
主要是用以下方法达到通信的效果:
wait():先释放资源,并开始等待wait(long timeout):释放资源等待timeout秒notify():唤醒一个处于等待状态的线程notifyAll():唤醒同一个对象上所有调用了wait()方法的线程,优先级高的优先调度
解决方式:
管程法
设置一个缓冲区,用于暂存数据
代码示例:
package cn.livorth.gaoji;import java.util.LinkedList;import java.util.Queue;//管程法解决public class TestPC {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;}@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println("生产了第" + i + "件产品");container.push(new Products(i));}}}//消费者class Consumer extends Thread{SynContainer container;public Consumer(SynContainer container) {this.container = container;}@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println("消费了第" + container.pop().id + "件产品");}}}//产品class Products{int id;public Products(int id) {this.id = id;}}//缓冲区class SynContainer{Queue<Products> queue = new LinkedList<Products>();int count = 0;int size = 10;//生产者放入产品public synchronized void push(Products product){//容器满,等待消费者消费if(count == size){try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}}//没满,我们则需要存入成品,并唤醒消费者count++;queue.offer(product);this.notifyAll();}//消费者消费产品public synchronized Products pop(){//容器为空,等待生产者生产if (count == 0) {try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}}//容器有剩,取出物品,并告诉生产者可以生产了count--;Products productPoll = queue.poll();this.notifyAll();return productPoll;}}
信号灯法
设置一个标记位(类似于容量为1的管程法)
package cn.livorth.gaoji;//信号灯法解决public class TestPC2 {public static void main(String[] args) {//创建缓冲区TheProduct theProduct = new TheProduct();//双线程new Producers(theProduct).start();new Consumers(theProduct).start();}}//生产者class Producers extends Thread{TheProduct theProduct;public Producers(TheProduct theProduct) {this.theProduct = theProduct;}@Overridepublic void run() {for (int i = 0; i < 10; i++) {theProduct.push("产品" + i);}}}//消费者class Consumers extends Thread{TheProduct theProduct;public Consumers(TheProduct container) {this.theProduct = container;}@Overridepublic void run() {for (int i = 0; i < 10; i++) {theProduct.pop();}}}//产品class TheProduct{boolean flag = false;String product;//生产者生产,消费者等待public synchronized void push(String product){if(flag){try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("生产者生产了" + product);this.product = product;this.flag = !this.flag;this.notifyAll();}//消费者消费,生产者等待public synchronized void pop(){if(!flag){try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("消费者消费了" + product);this.flag = !this.flag;this.notifyAll();}}
线程池
背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中,可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具好处: 提高响应速度(减少了创建新线程的时间)降低资源消耗(重复利用线程池中线程,不需要每次都创建)便于线程管理 corePoolSize:核心池的大小maximumPoolSize: 最大线程数keepAliveTime: 线程没有任务时最多保持多长时间后会终止简单的使用参照Callable的使用
并发进阶以后再学