一、线程的简介
Process与Thread
1、程序是指令和数据的有序集合,进程的一次执行过程,是一个动态的概念,是系统资源分配的单位
2、一个进程可以包含若干个线程,当一个进程中至少有一个线程,
3、线程是CPU调度和执行的单位
1、线程就是独立执行的路径
2、main()称之为主线程,为系统的入口,用于执行整个程序
3、在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的操作,4、先后顺序是不能人为的干预的。
5、对于同一份资源操作时,会存在资源抢夺的情况,需要加入并发控制
6、线程会带来额外的开销,如cpu调度时间,并发控制开销
7、每个线程在自己的工作内存交互,内存控制不当,会引起数据的不一致
run方法和start方法的对比
二、线程的实现(重点)
创建方法一:继承Thread
步骤:
1、自定义线程类继承Thread类
2、重写run()方法,编写线程执行体
3、创建线程对象,调用start()方法启动线程
//创建线程的方法一:继承Thread类,重写run()方法,调用start()方法开启线程
//注意,线程开启不一定运行,由CPU调度
public class TestThread1 extends Thread {
//线程入口
@Override
//run方法属于子线程
public void run() {
//run方法体(线程体)
for (int i = 0; i < 200; i++) {
System.out.print("我在看代码------" + i);
}
}
//main属于主线程
public static void main(String[] args) {
//创建线程对象
TestThread1 testThread1 = new TestThread1();
//调用start方法和调用run方法的区别
//调用start(),线程是同时进行运行的
//如果testThread1.run(),则先运行run在运行main()
testThread1.start();
for (int i = 0; i < 1000; i++) {
System.out.print("我在学习多线程------" + i);
}
}
}
//需要引用commons-io架包
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.print("下载的文件名称为:" + name);
}
public static void main(String[] args) {
String urltemp="https://t7.baidu.com/it/u=1819248061,230866778&fm=193&f=GIF";
TestThread2 testThread2 = new TestThread2(urltemp,"TESTNAME1");
testThread2.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.print("IO异常,downloader方法出现问题");
}
}
}
}
创建方法二:定义MyRunnable类实现Runnable接口
步骤
1、定义MyRunnable类实现Runnable接口
2、实现run()方法,编写线程执行
3、创建线程对象,调用start()方法启动线程
推荐使用线程对象、
对比第一种方式,推荐这种,因为java单继承具有局限性,其实Thread也是实现类Runnable接口,
步骤
继承Thread类
继承Thread类 | 实现Runnable接口 |
---|---|
子类继承Thread类具备多线程能能力 | 实现接口Runnable具有多线程能力 |
启动线程:子类对象。start() | 启动线程:传入目标对象+Thread对象。start() |
不建议使用,避免OOP单继承局限性 | 推荐使用,避免局限性,同一对象可被多个线程使用 |
public class TestThread3 implements Runnable {
//创建线程的方式2:实现runnable,重写run方法,执行线程需要丢入runnable接口实现类,调用start方法
@Override
public void run() {
for (int i = 0; i < 200; i++)
System.out.print("我在看代码----" + i);
}
public static void main(String[] args) {
//创建runnable接口的实现类对象
TestThread3 testThread3=new TestThread3();
//创建线程对象,通过线程对象来开启线程
//Thread thread = new Thread(testThread3);
//thread.start();
Thread t1 = new Thread(testThread3);
t1.start()//开启线程
for (int i = 0; i < 300; i++)
System.out.println("我在学习多线程" + i);
}
}
创建方法三:实现Callable接口(了解即可)
匿名内部类创建线程
匿名内部类方式实现线程的创建
作用:简化代码
把子类继承父类,重写父类方法,创建子类对象合成一步完成
把实现类实现接口,重写接口中的方法,创建实现类对象合成一步完成
匿名内部类的最终产物:子类/实现类对象,而这个类没有名字
格式:
new 父类/接口() {
重写父类方法
};
public static void main(String[] args) {
//线程的父类是Thread
//new MyThread().start();
new Thread(){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+ "-->"+i);
}
}
}.start();
//线程的接口
Runnable r = new Runnable(){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+ "-->"+"程序员");
}
}
};
new Thread(r).start();
//接口的线程简化
new Thread(new Runnable(){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+ "-->"+"林枫");
}
}
}).start();
}
静态代理
静态代理,,模式终结
要求:
1、真实对象和代理对象要实现同一个接口
2、代理对象必要代理真实角色
静态代理实际上就是线程的实现原理
好处:
1、代理对象可以帮助真实对象做很多事情
2、真实对象可以专注做好自己的事情
package com.company;
//静态代理,,模式终结
//真是对象和代理对象又要实现同一个接口
//代理对象必要要代理真实角色
//好处:
//1、代理对象可以帮助真实对象做很多事情
//2、真实对象可以专注做好自己的事情
public class StaticProxy{
public static void main(String[] args) {
You you=new You();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("我爱你");
}
}).start();
WeddingCompany weddingCompany = new WeddingCompany(you);
weddingCompany.HapplyMarry();
}
}
//定义一个接口,让真实对象实现
interface Marry {
void HapplyMarry();
}
class You implements Marry{
@Override
public void HapplyMarry(){
System.out.println("张三要结婚");
}
}
//代理角色,帮助你结婚
class WeddingCompany implements Marry{
//代理——真实目标
private Marry target;
public WeddingCompany(Marry target) {
this.target = target;
}
@Override
public void HapplyMarry() {
before();
this.target.HapplyMarry();//这是真实对象
after();
}
private void after(){
System.out.println("结婚之后,收取尾款");
}
private void before(){
System.out.println("结婚之前,布置现场");
}
}
Lamda表达式
public class TestLamda {
public static void main(String[] args) {
//使用匿名内部类
Ilove love=new Ilove(){
public void love(){
System.out.println("i love you");
}
};
love.love();
}
}
interface Ilove {
public void love();
}
//使用lamda表达式代替
public class TestLamda {
public static void main(String[] args) {
//使用匿名内部类
ILove ilove = () -> {
System.out.println("i love you");
};
ilove.love();
}
}
interface ILove {
void love();
}
三、线程的状态
NEW | 初始状态 | 尚未启动的线程处于此状态 |
---|---|---|
RUNNABLE | 运行状态 | 在JAVA虚拟机中执行的线程处于此状态,Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。 线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running) |
BLOCKED | 阻塞 | 被阻塞等待监视器的线程处于此状态 |
WAITING | 等待 | 在等待另一个线程执行动作到达指定等待时间的线程处于此状态 |
TIMED WAITING | 超时状态 | 正在等待另一个线程执行动作到达指定等待时间 |
TERMINATED | 终止 | 表示该线程已经执行完毕,已退出的线程处于此状态 |
几种方法的比较
- Thread.sleep(long millis),一定是当前线程调用此方法,当前线程进入TIMED_WAITING状态,但不释放对象锁,millis后线程自动苏醒进入就绪状态。作用:给其它线程执行机会的最佳方式。
- Thread.yield(),一定是当前线程调用此方法,当前线程放弃获取的CPU时间片,但不释放锁资源,由运行状态变为就绪状态,让OS再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield()不会导致阻塞。该方法与sleep()类似,只是不能由用户指定暂停多长时间。
- thread.join()/thread.join(long millis),当前线程里调用其它线程t的join方法,当前线程进入WAITING/TIMED_WAITING状态,当前线程不会释放已经持有的对象锁。线程t执行完毕或者millis时间到,当前线程一般情况下进入RUNNABLE状态,也有可能进入BLOCKED状态(因为join是基于wait实现的)。
- obj.wait(),当前线程调用对象的wait()方法,当前线程释放对象锁,进入等待队列。依靠notify()/notifyAll()唤醒或者wait(long timeout) timeout时间到自动唤醒。
- obj.notify()唤醒在此对象监视器上等待的单个线程,选择是任意性的。notifyAll()唤醒在此对象监视器上等待的所有线程。
LockSupport.park()/LockSupport.parkNanos(long nanos),LockSupport.parkUntil(long deadlines), 当前线程进入WAITING/TIMED_WAITING状态。对比wait方法,不需要获得锁就可以让线程进入WAITING/TIMED_WAITING状态,需要通过LockSupport.unpark(Thread thread)唤醒。
停止线程
注意:
1、不推荐使用JDK提供的stop()、destroy()方法。已经弃用
2、推荐线程自己停下来
3、建议使用一个标志位进行终止变量,当flag=false,则终止线程运行import java.awt.*;
public class TestStop implements Runnable {
//1、设置一个标志位
boolean flag=true;
int i = 0;
@Override
public void run() {
while (flag) {
System.out.println(i++);
}
}
//2、设置一个公开的方法停止线程,转换标志位
public void stopWhile() {
this.flag = false;
}
public static void main(String[] args) {
TestStop testStop = new TestStop();
new Thread(testStop).start();
for (int i = 0; i < 2; i++) {
System.out.println("main"+i);
if (i == 1) {
//调用stopWhile方法切换标志位,使得线程停止
testStop.stopWhile();
System.out.println("线程停止了");
}
}
}
}
线程休眠
Sleep(time)方法,指定当前线程阻塞的毫秒数
- sleep存在异常InterruptedException
- sleep时间达到后线程进入倒计时就绪状态
- sleep可以模拟网络延时效果,倒计时等
- 每个对象都有一个锁,sleep不会释放锁
作用:
1、模拟网络延时
2、模拟倒计时
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.logging.SimpleFormatter;
public class TestSleep2 {
public static void main(String[] args) {
//获取当前系统时间
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();
}
}
}
//模拟倒计时
public static void tenDown() throws InterruptedException {
int num = 10;
while (true) {
Thread.sleep(1000);
System.out.println(num--);
if (num <= 0) {
break;
}
}
}
}
线程的礼让
- 礼让线程,让当前的正在执行的线程暂停,但不阻塞
- 将线程从运行状态转为就绪状态
- 让CPU重新调度,礼让不一定成功,需要根据CPU实际的调度
线程的强制执行
Jion()方法合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞
可以想象成插队
线程状态的观测
Thread.State
线程状态,线程可以处于以下状态之一:
NEW
NEW | 尚未启动的线程处于此状态 |
---|---|
RUNNABLE | 在JAVA虚拟机中执行的线程处于此状态 |
BLOCKED | 被阻塞等待监视器的线程处于此状态 |
WAITING | 在等待另一个线程执行动作到达指定等待时间的线程处于此状态 |
TIMED WAITING | 正在等待另一个线程执行动作到达指定等待时间 |
TERMINATED | 已退出的线程处于此状态 |
一个线程可以在给定的时间点处于一个状态,这些状态是不反映任何操作系统线程状态的虚拟机状态
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);
}
}
}
线程的优先级
JAVA提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行
线程的优先级用数字表示。范围从1-10
Thread. MIN_PRIORITY | 1 |
---|---|
Thread. MAX_PRIORITY = 10 | 10 |
Thread. NORM_PRIORITY | 5 |
1、优先级低只是意味着获得调度的概率低,并不是优先级低就不会被调用,取决于CPU调度
2、优先级的更改建议在start()之前
使用以下方法可以改变或获取优先级
getPriority().stePriority(int XXX);
优先级最大是10,最小是1,默认程序的优先级是5
//测试线程的优先级
public class TestPriority implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"----"+Thread.currentThread().getPriority());
}
public static void main(String[] args) {
TestPriority testPriority = new TestPriority();
Thread t1 = new Thread(testPriority,"t1");
Thread t2 = new Thread(testPriority,"t2");
Thread t3 = new Thread(testPriority,"t3");
Thread t4 = new Thread(testPriority,"t4");
Thread t5 = new Thread(testPriority,"t5");
//设置优先级
t2.setPriority(1);
t3.setPriority(Thread.MAX_PRIORITY);
t4.setPriority(6);
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
}
}
守护线程
daemon,守护线程,
线程分为,用户线程和守护线程
1、虚拟机必须确保用户线程执行完毕
2、虚拟不用等待守护线程执行完毕
3、如后台记录操作日志,监控内存,垃圾回收等待等
四、线程的同步(重点)
龟兔赛跑
package com.company;
public class Race implements Runnable {
private static String winner;
@Override
public void run(){
//预设跑道长度是100米
for (int i = 0; i <= 100; i++) {
//判断比赛是否结束
boolean flag = gameOver(i);
if (flag){
System.out.println(i);
break;
}
System.out.println(Thread.currentThread().getName()+"-->跑了"+i+"米");
//模拟兔子睡觉
if (Thread.currentThread().getName().equals("兔子") && i % 10 == 0) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
private boolean gameOver(int steps) {
if (winner != null) {
return true;
}else if (steps >= 100) {
winner = Thread.currentThread().getName();
System.out.println(winner+"赢得比赛");
return true;
}
return false;
}
public static void main(String[] args) {
Race race = new Race();//
new Thread(race,"乌龟").start();//乌龟的线程实例对象
new Thread(race, "兔子").start();//兔子的线程实例对象
}
}
并发问题
不安全的买票
package com.company;
public class SafeBank implements Runnable{
private int ticketNums = 10;
Boolean flag = true;//外部停止条件
@Override
public void run() {
while (flag){
try {
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void buy() throws InterruptedException {
//判断是否售完票
if (ticketNums == 0) {
flag = false;
return;
}
//模拟延时
Thread.sleep(10);
//输出抢到票的结果
System.out.println(Thread.currentThread().getName()+"-->拿到了第" + ticketNums-- + "张票");
}
public static void main(String[] args) {
SafeBank safeBank = new SafeBank();
new Thread(safeBank,"张三").start();
new Thread(safeBank, "黄牛").start();
}
}
package com.company;
//输出结果是有人拿到了同一张票
public class TestThead2 implements Runnable {
private int ticketNums = 10;
@Override
public void run() {
while (true) {
//判断是否售完票
if (ticketNums <= 0) {
break;
}
//模拟延时
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//输出抢到票的结果
System.out.println(Thread.currentThread().getName()+"-->拿到了第" + ticketNums-- + "张票");
}
}
public static void main(String[] args) {
TestThead2 testThead2 = new TestThead2();
new Thread(testThead2,"张三").start();
new Thread(testThead2, "黄牛").start();
}
}
不安全的数组
import java.util.ArrayList;
import java.util.List;
//不安全的数组
public class UnsafeList {
public static void main(String[] args) {
final List<String> list = new ArrayList<String>();
for (int i = 0; i < 2; i++) {
new Thread(new Runnable() {
@Override
public void run() {
list.add(Thread.currentThread().getName());
}
}).start();
}
System.out.print(list.size());
}
}
不安全的银行取款
public class UnsafeBank {
public static void main(String[] args) {
//账户
Accout accout = new Accout(100, "the fund");
Drawing you=new Drawing(accout,50,"you");
Drawing grilFrriend = new Drawing(accout, 100, "GF");
you.start();
grilFrriend.start();
}
}
class Accout{
int money;//余额
String name;//卡名
public Accout(int money, String name) {
this.money = money;
this.name = name;
}
}
class Drawing extends Thread{
Accout accout;//账户
int drawingMoney;//取了多少钱
int nowMoney;//现在手上有多少钱
public Drawing(Accout accout, int drawingMoney, String name) {
super(name);
this.accout = accout;
this.drawingMoney = drawingMoney;
}
//取钱
@Override
public void run() {
//判断有没有钱
if (accout.money - drawingMoney < 0) {
System.out.print(Thread.currentThread().getName() + "钱不够,取不了");
return;
}
//卡内余额=余额-你取出来的钱
accout.money = accout.money - drawingMoney;
//你手里的钱
nowMoney = nowMoney + drawingMoney;
System.out.println(accout.name + "余额:" + accout.money);
//Thread.currenthread().getName() 等价于 this.getName()
System.out.println(this.getName()+"手里的钱");
//Thread.currenthread().getName() 等价于 this.getName()
System.out.println(this.getName());
}
}
队列和锁
同步方法
由于我们可以通过private关键字来保证数据对象只能被方法访问,所以只需要针对方法提出一套机制,这套机制就是synchroized关键字,包括两种方法:
synchronized关键字修饰和synchronized块
public synchronized void method(int args){}
//syhchronized方法控制对象的访问,每个对象对应一把锁,每个syhchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程就会阻塞,方法一旦执行,就独占该锁,知道方法返回才会释放锁,后面的被阻塞的线程才能获得这个锁,继续执行
缺陷:若将一个大的方法声明为synchroized将会影响效率,synchronized方法只锁定当前this对象,
安全的购票方式
package com.company;
public class SafeBank implements Runnable{
private int ticketNums = 10;
Boolean flag = true;//外部停止条件
@Override
public void run() {
while (flag){
try {
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void buy() throws InterruptedException {
//判断是否售完票
if (ticketNums == 0) {
flag = false;
return;
}
//模拟延时
Thread.sleep(10);
//输出抢到票的结果
System.out.println(Thread.currentThread().getName()+"-->拿到了第" + ticketNums-- + "张票");
}
public static void main(String[] args) {
SafeBank safeBank = new SafeBank();
new Thread(safeBank,"张三").start();
new Thread(safeBank, "黄牛").start();
}
}
同步块
同步块:sychronized(Objr){}
Obj称之为同步监视器
obj可以是任意对象了,但是推荐使用共享资源作为同步监视器
同步方法中无需指定同步监视器,因为同步方法中同步监视器是this,就是对象本身,或者是claa(反射中讲解)
同步监视器的执行过程:
1、第一个线程访问,锁定同步监视器,执行其中的代码
2、第二个线程访问,发现同步监视器被锁定,无法访问
3、第一个线程访问结束,解锁同步监视器
4、第二个线程访问,发现同步监视器没有锁,然后锁定并继续访问
五、线程通信
六、高级主题
线程池
背景:经常创建喝销毁,使用量特别大的资源,比如并发情况下的线程,对性能影响很大
思路:提前创建好多个线程,放入线程池中吗,使用时直接获取,使用完毕放回池中,可以避免频繁创建和销毁,实现重复利用,
好处:
- 提高响应速度,减少创建线程的时间
- 降低资源消耗
便于线程管理
JDK5.0,提供了线程相关的API:ExecutorService和Executors
- ExrcutorService:真正的线程池的接口,常见的子类ThreadPoolExecutor
- void