前言
提示:狂神多线程教程的笔记,加上自己理解.
如有不足,请多指正
线程与进程
线程
-
线程就是独立的执行路径,是cpu调度和执行的单位
-
线程是序执行流中最小执行单位,是进程中实际运行单位。
-
main()称为主线程,为系统的入口,用于执行整个程序
-
一个进程中如果开辟了多个线程,线程的运行是由调度器安排的。调度器是与操作系统紧密相关的,先后顺序看CPU心情,不能人为干预
-
线程对共享资源进行操作时,会出现安全问题,需要加入并发控制
-
线程会消耗资源,如CPU调度时间,并发控制资源开销
-
每个线程会在自己的工作内存中进行交互,内存控制不当会造成数据不一致问题
进程
- 进程是执行程序的一次执行过程,是一个动态的过程,是一个活动的实体,是系统资源分配的单位。
- 一个应用程序的运行就可以被看做是一个进程,而线程,是运行中的实际的任务执行者。可以说,进程中包含了多个可以同时运行的线程。
举一个简单的例子
你打开一个视频,视频应用程序运行就可以被看做一个进程。
而视频播放时,视频的画面,音频,弹幕,都是独立的执行路径,这些就可以被看做一个进程
线程的创建
线程的三种创建方式
1.继承Thread类
-
继承Thread类
-
重写run()方法,编写线程体
-
然后通过start()启动线程(线程开启不一定立即执行,由cpu进行调度)
//1.继承Thread类
public class ThreadTest extends Thread {
//2.重写run()方法,编写线程体
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "线程方法被调用了");
}
public static void main(String[] args) {
ThreadTest threadTest = new ThreadTest();
//3.然后通过start()启动线程
threadTest.start();
}
}
继承Thread类的缺点,oop单继承的局限性,于是有了Runnable接口
推荐使用实现Runnable接口,方便同一对象被多个线程使用,实现资源共享
2.实现Runnable接口
-
实现Runable接口
-
重写Run()方法,编写线程体
-
创建Runnable对象
-
再通过Thread进行静态代理,启动线程
//1.实现Runnable接口
class TestRunnable implements Runnable {
//2.重写Run()方法,编写线程体,
@Override
public void run() {
System.out.println("实现了run方法");
}
}
public static void main(String[] args) {
ThreadTest threadTest = new ThreadTest();
threadTest.start();
//3.创建Runnable对象
//4.通过Thread进行静态代理,启动线程
Thread thread = new Thread(new TestRunnable());
thread.start();
}
3.实现Callable接口
实现Callable接口
重写call方法
线程状态
线程的五大状态
- 创建状态
- 就绪状态
- 阻塞状态
- 运行状态
- 死亡状态
获取线程状态
线程常用方法
线程方法 | 使用说明 |
---|---|
static void sleep(long millis) | 在指定的毫秒数内让当前正在执行的线程休眠 |
static void yield() | 暂停当前正在执行的线程对象,并执行其他线程 |
void join() | 插入调用该方法的线程对象,直至该线程终结 |
void interrupt | 中断线程,不建议使用 |
setPriority(int newPriority) | 更改线程的优先级,用数字来表示,范围从1~10 。10最高,默认为5 |
boolean isAlive | 测试线程是否处于活动状态 |
停止线程
- jdk中提供stop()、destroy()方法,不建议使用,已废弃
- 推荐使用一个标志位,终止线程的运行
class DemoStop implements Runnable {
//定义线程体使用的标识
private boolean flag = true;
@Override
public void run() {
int i = 0;
//线程体中使用该标识
while (flag) {
System.out.println("run===Thread" + i++);
}
}
public void stop() {
this.flag = false;
System.out.println("线程停止");
}
}
public class TestStop {
public static void main(String[] args) {
DemoStop demoStop = new DemoStop();
new Thread(demoStop).start();
for (int i = 0; i < 1000; i++) {
System.out.println(i);
//main线程中,i=500时,通过调用stop方法改变标志位,从而停止线程
if (i == 500) {
demoStop.stop();
}
}
}
}
线程休眠sleep()
- sleep(毫秒数)指定当前线程停止的时间,进入阻塞状态
- sleep()时间到达后,被自动唤醒,线程进入就绪状态
- sleep()存在异常InteruptedException(中断异常)
- sleep()可以模拟网络延时,倒计时等,用于放大线程问题的发生性
- 使用sleep()时,不会释放锁资源
- sleep()可以在任意位置使用
//用slee()方法打印当前时间
class DemoSleep implements Runnable {
Date startTime = new Date(System.currentTimeMillis());
@Override
public void run() {
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 class TestSleep {
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();
}
}
}
}
线程的礼让yield()
- 礼让线程,让当前正在执行的线程暂停,但不阻塞,进入就绪状态
- 让CPU重新调度,下一次依旧可能调用到原来的礼让线程,所以礼让不一定成功,看CPU心情
class DemoYield implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"线程开始");
//线程进行礼让
Thread.yield();
System.out.println(Thread.currentThread().getName()+"线程结束");
}
}
public class TestYield {
public static void main(String[] args) {
DemoYield demoYield = new DemoYield();
new Thread(demoYield,"线程A").start();
new Thread(demoYield,"线程B").start();
}
}
线程强制执行join()
- Join合并线程,待此线程执行完成后,在执行其他线程,其他线程阻塞
- 可以想象成银行排队时插队,vip有特权,优先执行,有钱真好
class DemoJoin implements Runnable{
@Override
public void run() {
for (int i = 0; i <1000 ; i++) {
System.out.println("vip来了"+i);
}
}
}
public class TestJoin {
public static void main(String[] args) throws InterruptedException {
DemoJoin demoJoin = new DemoJoin();
Thread thread = new Thread(demoJoin);
thread.start();
for (int i = 0; i <800 ; i++) {
//当main线程执行到200时,thread线程就来插队了,main线程阻塞,等thread线程执行完,才轮到main
if (i==200){
thread.join();
}
System.out.println("main"+i);
}
}
}
线程的优先级setPriority(int newPriority)
-
java 提供一个线程调度器来监视程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定该调度哪个线程来执行
-
线程的优先级用数字来表示,范围从110。优先级从101依次递减,线程默认的优先级为5
Thread.MIN_PRIORITY = 1(最小优先级)
Thread.MAX_PRIORITY= 10(最大优先级)
Thread.NORM_PRIORITY = 5(默认优先级)
-
使用getPriority()和setPriority()来获取或改变优先级
-
不是优先级越高,线程就先执行。优先级反映的是线程占用资源的多少,优先级越高,占用的资源越多,性能越好,被cpu优先调度的可能性就越高
class DemoPriority implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"=====>>>"+
Thread.currentThread().getPriority());
}
}
public class TestPriority {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName()+"=====>>>"+
Thread.currentThread().getPriority());
DemoPriority demoPriority = new DemoPriority();
Thread thread1 = new Thread(demoPriority,"线程1");
Thread thread2 = new Thread(demoPriority,"线程2");
Thread thread3 = new Thread(demoPriority,"线程3");
Thread thread4 = new Thread(demoPriority,"线程4");
thread1.setPriority(Thread.MIN_PRIORITY);//1
thread1.start();
thread2.setPriority(Thread.MAX_PRIORITY);//10
thread2.start();
thread3.setPriority(Thread.NORM_PRIORITY);//5
thread3.start();
thread4.setPriority(6);//6
thread4.start();
}
}
守护线程setDeamon()
- 线程分为用户线程和守护线程
- 虚拟机必须确保用户线程执行完毕
- 虚拟机不用等待守护线程执行完毕,如后台记录操作日志,监控内存,垃圾回收等
//你
class You implements Runnable{
@Override
public void run() {
for (int i = 0; i < 36500; i++) {
System.out.println("开心生活每一天");
}
}
}
//上帝守护着你
class God implements Runnable{
@Override
public void run() {
while(true){
System.out.println("上帝保佑着你");
}
}
}
public class TestDeamo {
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();
}
}
线程同步
对象共享问题
处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象,这时候我们就需要线程同步。线程同步其实是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕,下一个线程在使用
由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问的冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制syncronized,当一个线程获得对象的排他锁,独占资源,其他线程必须等待,使用后释放锁即可,存在一下问题
- 一个线程持有锁会导致其他所有需要此锁的进程挂起
- 在多线程竞争的情况下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题
- 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题
对象锁是在一个类的对象上加的的锁,只有一把,不管有几个方法进行了同步。
这些同步方法都共有一把锁,只要一个线程获得了这个对象锁,其他的线程就不能访问该对象的任何一个同步方法
同步方法和同步代码块
同步方法
同步方法在方法上添加synchronized关键字, 由于java的每个对象都有一个内置锁,锁的是对象本身。当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
注:synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类
例子1
class DemoSynchronized implements Runnable {
private int ticketNums = 100;
private boolean flag = true;
@Override
public void run() {
while (flag){
try {
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void buy() throws InterruptedException {
while (ticketNums<=0){
flag = false;
System.out.println("票卖完了");
return;
}
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+ "拿票"+ticketNums--);
}
}
public class TestSynchronized {
public static void main(String[] args) {
DemoSynchronized demoSynchronized = new DemoSynchronized();
new Thread(demoSynchronized,"苦逼的我").start();
new Thread(demoSynchronized,"牛逼的你").start();
new Thread(demoSynchronized,"可恶的黄牛").start();
}
}
同步代码块
synchronized关键字修饰的语句块。被该关键字修饰的语句块会自动加上内置锁,从而实现同步
同步块: synchronized(obj){}
obj称之为同步监视器
- obj可以是任何对象,但是推荐使用共享资源作为同步监视器
- 同步方法中无需指定同步监视器,因为同步方法中的同步监视器就是this,就是这个对象本身,或者是class
同步监视器的执行过程
- 第一个线程访问,锁定同步监视器,执行其中的代码
- 第二个线程访问,发现同步监视器被锁定,无法访问
- 第一个线程访问完毕,解锁同步监视器
- 第二个线程访问,发现同步监视器没有锁,然后锁定并访问
例子二
//不安全取钱
public class UnsafeBank {
public static void main(String[] args) {
//账户,两个人去银行取钱
Account account=new Account(100,"结婚基金");
Drawing you=new Drawing(account,50,"你");
Drawing girlFriend=new Drawing(account,100,"女朋友");
you.start();
girlFriend.start();
}
}
//账户
class Account{
int money;//余额
String name;//卡名
public Account(int money, String name) {
this.money = money;
this.name = name;
}
}
//银行:模拟取款
class Drawing extends Thread{
Account account;//账户
//取了多少钱;
int drawingMoney;
//现在手里又多少钱
int nowMoney;
public Drawing(Account account,int drawingMoney,String name){
super(name);
this.account=account;
this.drawingMoney=drawingMoney;
}
//取钱
@Override
public void run() {
synchronized (account) {//锁的对象是变化的量,锁需要增删改的对象
//判断有没有钱
if (account.money - drawingMoney <= 0) {
System.out.println(Thread.currentThread().getName() + "钱不够");
return;
}
//卡内余额
account.money -= drawingMoney;
//手里的钱
nowMoney += drawingMoney;
System.out.println(account.name + "余额为:" + account.money);
System.out.println(this.getName() + "手里的钱:" + nowMoney);
}
}
}
不同线程的实例对象不同,都是各自对象的锁
例子三
public class UnsafeList {
public static void main(String[] args) {
ArrayList<String> list=new ArrayList<String>();
for (int i = 0; i <1000 ; i++) {
new Thread(()->{
synchronized (list){
list.add(Thread.currentThread().getName());
}
}).start();
}
System.out.println(list.size());
}
}
因为 ArrayList 不是线程安全的,在高并发情况下对list进行数据添加会出现数据丢失的情况。
main线程在遍历List,另一个线程修改List。
一个线程在遍历List,另一个线程修改List,可能会报ConcurrentModificationException(并发修改异常)错误
死锁
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源释放才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形,某一个同步块同时拥有"两个以上的对象锁"时,就可能发生死锁现象
死锁产生的条件
- 互斥条件: 一个资源每次只能被一个进程使用
- 请求保持条件: 一个进程因请求资源而阻塞时.对以获得的资源保持不放
- 不剥夺条件: 进程已获得的资源,在未使用完之前,不能强行剥夺
- 循环等待条件: 若干进程之间形成一种头尾相接的循环等待资源关系
Lock锁
-
从JDK1.5开始,java提供了更为强大的线程同步机制——通过显示定义同步锁对象来实现同步,同步锁使用lock对象来充当
-
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
-
ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中。比较常用的是ReentrantLock,可以显示加锁,释放锁
public class TestLock {
public static void main(String[] args) {
TestLock2 testLock2 = new TestLock2();
new Thread(testLock2).start();
new Thread(testLock2).start();
new Thread(testLock2).start();
}
}
class TestLock2 implements Runnable {
int ticketNums = 10;
//定义Lock锁
private ReentrantLock reentrantLock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
reentrantLock.lock(); //加锁
if(ticketNums > 0 ){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "购买了 第" + ticketNums-- + "张票");
} else {
break;
}
} finally {
//解锁
reentrantLock.unlock();
}
}
}
}
公平锁
创建互斥锁对象的时候,通过构造方法传递 true 的时候,会成为公平锁
public class Demo01 {
public static void main(String[] args) throws InterruptedException {
Girl girl = new Girl();
for (int i = 1; i < 11; i++) {
//第二参数为线程的名字
new Thread(girl,"第"+i+"个小女孩").start();
Thread.sleep(100);
}
}
}
//任务类对象
class Girl implements Runnable{
//卖出去的核弹
private int bomb = 0;
private static final Object obj = new Object();
//当创建互斥锁对象的时候,通过构造方法传递 true 的时候,会成为公平锁
private Lock myLock = new ReentrantLock(true);
@Override
public void run() {
try {
sellBomb();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 卖核弹的方法
*/
private void sellBomb() throws InterruptedException {
while(true){
Thread.sleep(1);
//获取锁
myLock.lock();
try{
//无法使用wait()
//myLock.wait();
if(bomb == 300){
System.out.println(Thread.currentThread().getName() + "卖出第" + (++bomb) + "个核弹,不卖回家了");
break;
}else if(bomb == 500){
System.out.println(Thread.currentThread().getName() + "卖出第" + (++bomb) + "个核弹,也不卖回家了");
break;
} else if(bomb < 1000) {
System.out.println(Thread.currentThread().getName() + "卖出第" + (++bomb) + "个核弹");
}else{
System.out.println(Thread.currentThread().getName()+"卖完了");
break;
}
//为了增长同步的线程的运行时间
Thread.sleep(5000);
}finally {
//释放锁
myLock.unlock();
}
}
}
}
synchronized 和 Lock的区别
- 同步锁是关键词,Lock是接口
- 同步锁形参的同步区域可以调用 wait() 使线程进入阻塞状态,使用 Lock 无法使用 wait()
- 同步锁是隐式获取锁释放锁,Lock 是显式获取锁和释放锁
关于多线程访问同步方法的7种情况
- 多个线程访问一个实例对象的同步方法
- 多个线程访问一个静态的同步方法
- 多个线程访问多个静态的同步方法
- 多线程访问一个实例对象的不同的同步方法
- 一个线程同时访问同步方法和非同步方法
- 一个线程同时访问多个不同的同步方法
- 同步方法抛出异常,锁的释放
线程协作
线程通信的常用方法
方法名 | 作用 |
---|---|
wait() | 表示线程一直等待,直到其他线程通知,与sleep不同,不会释放锁 |
wait(long timeout) | 指定等待的毫秒数 |
notify() | 唤醒一个处于就绪状态的线程 |
notifyAll() | 唤醒同一个对象所有调用wait()方法的线程,优先级别高的线程优先调度 |
以上方法都是Object方法,都只能在同步方法或者同步代码块中使用,否则会抛出IIIegaIMonitorStateException(非法的监视状态)异常
线程的等待wati()
- wati()来自于Object(),只能在同步区域内使用
- 使用wait()方法,线程在同步方法区中进行等待,会释放资源
- wait不会自动唤醒,只能通过notify()或者notifyAll()进行唤醒
线程的唤醒notify()和notifyALL()
- wait()只能通过notify唤醒,notify唤醒的是第一个进入等待的线程,先进先出
- notifyAll()唤醒所有线程我,最后进入等待的线程最先被唤醒:后进先出
//卖核弹的小女孩
public class Demo01 {
public static void main(String[] args) {
Girl girl = new Girl();
for (int i = 1; i < 11; i++) {
//第二参数为线程的名字
new Thread(girl,"第"+i+"个小女孩").start();
}
}
}
//任务类对象
class Girl implements Runnable{
//卖出去的核弹
private int bomb = 0;
//同步监视器
private static final Object obj = new Object();
@Override
public void run() {
try {
sellBomb();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 卖核弹的方法
*/
private void sellBomb() throws InterruptedException {
while(true){
Thread.sleep(1);
synchronized (obj){
//卖出去的核弹小于1000
if(bomb < 1000){
System.out.println(Thread.currentThread().getName() + "卖出第" + (++bomb) + "个核弹");
}else{
System.out.println(Thread.currentThread().getName()+"卖完了");
break;
}
//在同步区域中,使当前线程进入阻塞状态 -- 休眠
//当前线程没有释放资源,CPU 还是调用当前线程
//Thread.sleep(10000);
//在同步区域中,使当前线程进入阻塞状态 -- 等待
//当前线程会释放资源,CPU 会调用其他线程
obj.wait();
}
//在同步区域外,使当前线程进入阻塞状态 -- 等待
//会出现异常
//obj.wait();
}
}
}
管程法
并发协作模式"生产者/消费者模式" ===>管程法
- 生产者: 负责生产数据的模块(可能是方法,对象,线程,数组)
- 消费者: 负责处理数据的模块(可能是方法,对象,线程,数组)
- 缓冲区: 消费者不能直接使用生产者的数据,他们之间有个缓冲区
核心:生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据
//创建一个产品类
class Product {
private int id;
private String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Product(int id) {
this.id = id;
}
}
//创建一个生产者对象
class ProviderThread extends Thread {
private SyncContainer syncContainer;
//实例化的时候创建方法区
ProviderThread(SyncContainer syncContainer) {
this.syncContainer = syncContainer;
}
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println("生产了第" + i + "只鸡!");
try {
syncContainer.push(new Product(i));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//创建一个容器类,用于存放数据
class SyncContainer {
//容器大小
Product[] products = new Product[10];
//容器计数器
int count = 0;
//生产者,放入产品
public synchronized void push(Product product) throws InterruptedException {
//如果容器满了,就要等待消费者。通知消费者消费,生产者等待
if (count == products.length) {
this.wait();
}
//如果容器没有满,就要生产产品
products[count] = product;
count++;
//唤醒等待的线程
this.notifyAll();
}
//消费者消费产品
public synchronized Product pop() throws InterruptedException {
//判断容器是否为空,通知生产者生产,消费者等待
if (count == 0) {
this.wait();
}
count--;
Product product = products[count];
this.notifyAll();
return product;
}
}
//创建一个消费者类
class ConsumerThread extends Thread {
private SyncContainer syncContainer;
//实例化的时候创建方法区
ConsumerThread(SyncContainer syncContainer) {
this.syncContainer = syncContainer;
}
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
Product pop = null;
try {
pop = syncContainer.pop();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消费了第" + pop.getId() + "号鸡");
}
}
}
public class Main {
public static void main(String[] args) {
SyncContainer container = new SyncContainer();
new ProviderThread(container).start();
new ConsumerThread(container).start();
}
}
信号灯法
并发协作模式"生产者/消费者模式" ===>信号灯法
//产品,电视节目
class Tv {
//表演的节目
String voice;
//信号标志位,true等待 false通知
boolean flag = true;
//表演
public synchronized void play(String voice) {
if (!flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("演员该表演" + voice);
this.voice = voice;
this.notifyAll();
this.flag = !flag;
}
//观看
public synchronized void watch() {
if (flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("观众观看了" + voice);
//通知演员表演
this.notifyAll();
this.flag = !flag;
}
}
}
//演员
class Player extends Thread {
private Tv tv = null;
public Player(Tv tv) {
this.tv = tv;
}
public void Player(Tv tv) {
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
tv.play(i + "号节目");
}
}
}
//观众
class Watcher extends Thread {
private Tv tv = null;
public Watcher(Tv tv) {
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
tv.watch();
}
}
}
public class TestLight {
public static void main(String[] args) {
Tv tv = new Tv();
new Watcher(tv).start();
new Player(tv).start();
}
}
线程池
-
背景:线程经常创建和销毁,使用量特别大,比如并发情况显示的线程,对性能影响很大
-
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完毕放回池中,可以避免频繁的创建和销毁,实现重复利用,类似生活中的公共交通工具
-
好处
提高了响应速度(减少了线程的创建时间)
降低资源的消耗(重复利用线程池中的线程,不需要每次使用都创建)
便于线程的管理
corePoolSize:核心池的大小
maximumPoolSize:最大线程数
keepAliveTime:线程没有任务时最多保持多长时间后会终止