创建线程的方法
1、继承Thread类
格式如下:
public class 类名 extends Thread {
// 重写run()方法 (不要重写其他方法,不然运行程序会出bug)
@Override
public void run() {
// TODO Auto-generated method stub
}
}
2、实现Runnable接口
Runnable接口源码:
package java.lang;
/**
* @author Arthur van Hoff
* @see java.lang.Thread
* @see java.util.concurrent.Callable
* @since JDK1.0
*/
@FunctionalInterface // 函数式接口(可以使用lambda)
public interface Runnable {
public abstract void run();
}
格式如下:
public class TestRunnable implements Runnable {
// 必须重写run()方法
@Override
public void run() {
// TODO Auto-generated method stub
}
}
// 使用匿名内部内创建
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
// 返回线程ID
Thread.currentThread().getId()
// 返回线程名称
Thread.currentThread().getName();
// 返回线程状态
Thread.currentThread().getState()
// 返回线程的优先级 (默认优先级:5 最小优先级:1 最大优先级:10)
Thread.currentThread().getPriority()
}
}, "线程名");
}
// 使用lambda实现
public static void main(String[] args) {
new Thread(()->{
// 方法体
}, "线程名");
}
线程优先级
MAX_PRIORITY // 最大优先级 10
MIN_PRIORITY // 最小优先级 1
NORM_PRIORITY // 默认优先级 5
threadName.setPriority(Thread.MAX_PRIORITY); // 设置最大优先级 10
threadName.setPriority(Thread.MIN_PRIORITY); // 设置最小优先级 1
threadName.setPriority(Thread.MIN_PRIORITY); // 设置默认优先级 5
线程状态
1. 新建状态(New)
2. 就绪状态(Runnable)
3. 运行状态(Running)
4. 阻塞状态(Blocked)
5. 死亡状态(Dead)
线程状态转换:
包含了等待状态的线程状态转换图:
线程阻塞的方法:sleep() 和 join()
sleep() 方法
让线程进入休眠状态。
sleep(毫秒数):使当前正在执行的线程以指定的毫秒数暂停。(设置线程休眠必须抛异常)
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 15; i++) {
System.out.println(Thread.currentThread().getName() + "第:" + i + "次");
// sleep(毫秒):线程休眠
Thread.sleep(1000); // 设置当前线程休眠1秒
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "线程1").start();// start():开始执行此线程; Java虚拟机调用此线程的run方法。
}
join() 方法
join():等待这个线程死亡。
主线程中有多个子线程,等子线程执行完后,在结束主线程。
通常用在main()方法内。(使用join()方法必须抛异常)
主线程和一个A线程,如果A线程调用了join()方法,就会将A线程何如到主线程中,首先执行主线程,主线程执行过程中会启动A线程,A线程在运行过程中,主线程进入阻塞状态,等到A线程运行完毕,在执行主线程。
例:
public static void main(String[] args) {
System.out.println("Main start");
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "第:" + i + "次");
}
}
}, "线程1");
Thread threadTwo = new Thread(()->{
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "第:" + i + "次");
}
}, "线程2");
threadOne.start(); // 执行线程1
threadTwo.start(); // 执行线程2
try {
threadOne.join();
threadTwo.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Main end");
}
线程Lambda表达式
Runnable接口是一个函数式接口,可以为Rannable接口编写匿名函数
public class TestLambda {
public static void main(String[] args) {
for(int i=0;i<4;i++) {
new Thread(()->{
for(int j=1;j<=10;j++) {
System.out.println(Thread.currentThread().getName()+":::"+j);
}
}, "递增线程"+(i+1)).start();
}
}
}
如何停止一个线程
1. stop():强制停止线程,工作中不要使用
2. 使用定时器去停止正在执行的线程,定时器是一个监听者,监听线程。
定时器:设定一个周期,按照指定的周期重复的执行任务。
定时器有一个定时任务(实现了Runnable接口)。
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
/**
* 定时器每隔1秒钟打印一次系统当前时间
*/
public class TestTimerr {
public static void main(String[] args) {
Timer timer = new Timer();
// 参数1:定时器里面的定时任务;
// 参数2:delay延迟多长时间执行
/*timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println(new Date());
}
}, 1000); */
// Java的定时器:指定一个周期重复执行
// 参数1:定时器里面的定时任务
// 参数2:delay延迟多长时间执行(此时表示当前时间之后的1秒钟开始执行定时任务)
// 参数3:每次任务执行的间隔周期(此时表示每个2秒执行一次定时任务)
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println(new Date());
}
}, 1000,2000);
}
}
场景:main线程中启动一个ThreadA,同时开启一个定时任务去监听ThreadA,定时任务每间隔5秒钟扫描当前工程有没有stop文件,如果有将ThreadA线程停止掉。
import java.io.File;
import java.util.Timer;
import java.util.TimerTask;
public class TestStop {
public static void main(String[] args) {
StopTask stop = new StopTask(true);
Thread stopThread = new Thread(stop);
stopThread.start();
// 创建定时器,定时器包含一个定时任务,每个5秒钟检查当前工程下面有没有stop文件,如果有停止线程
Timer timer = new Timer();
// schedule()方法调度定时任务的执行
timer.schedule(new TimerTask() {
@Override
public void run() {
File file = new File("stop");
// 条件成立:表示stop文件存在,停止线程
if(file.exists()) {
stop.setFlag(false);
// StopTask任务停止,立马删除stop文件
file.delete();
// 取消定时任务
timer.cancel();
}
}
}, 1000,5000);
}
}
class StopTask implements Runnable{
private boolean flag ;
public void setFlag(boolean flag) {
this.flag = flag;
}
public StopTask(boolean flag){
this.flag = flag;
}
@Override
public void run() {
long index=0;
for(;flag;) {
index++;
if(index%500000000==0) {
System.out.println(Thread.currentThread().getName()+":::"+index);
}
}
}
}
守护线程
线程:执行线程(用户线程)、守护线程(守护执行线程的,一旦执行线程结束了,守护线程也会结束)
通常JVMGC线程就是一个守护线程
// 设置t1线程为守护线程
t1.setDaemon(true);
同步
线程安全和非线程安全
非线程安全:多个线程同一时刻访问一个共享资源,造成共享资源的数据出现脏数据,不安全(数组下标越界)
线程安全:需要对共享资源上一把锁,确保同一时刻最多只能有一个线程访问共享资源,其他线程在外面等待,不会出现脏数据。
synchronized 关键字
同步:对共享资源进行同步,确保同一时刻最多只能有一个线程访问共享资源
共享资源同步特征:确保共享资源在每个线程使用过程中数据的有效性和一致性,避免脏数据
同步机制:为共享资源上锁
synchronize就是为共享资源上锁,确保同一时刻最多只能有一个线程访问共享资源
小结:
-
工作中共享资源必须加锁
-
为了提供程序在内存中运行的效率,尽量不要锁住整个方法,而是锁代码块
-
那些代码块需要上锁?
first:某个共享资源的数据经常会发生改变
second:被多个线程使用
-
使用synchronize修饰某个块代码,叫做同步块
-
被synchronize修饰的代码叫做临界区(临界区的代码块最多只能有一个线程访问)
-
synchronize是一个JVM级别的互斥锁(排它锁)
JVM帮你加锁和解锁(不是人为加锁和解锁)
缺点:一旦出现异常可能无法解锁,因为不是你手工加锁
-
同步块括号不能少,括号里面的对象表示你要锁住的共享资源,最好使用final修饰,因为不可改变的
-
synchronize括号里面的对象不支持基本类型
Lock 重入锁
@SinceJDK1.5,重入锁可以进行加锁和解锁,比synchronize更加友好,一旦出现异常可以人工解锁
特征:有一个公平机制,等待时间最长的线程优先进入共享资源
创建格式如下:
//true:启动公平锁 false:非公平锁 默认false
private Lock lck = new ReentrantLock(true);
使用Lock格式如下:
// 加锁
lck.lock();
try{
}finally{
// 解锁
lck.unlock();
}
// 好处:一旦try块出现异常执行finally,将锁解掉
// 解锁操作工作中一定要放在finally块中,出现异常立马解锁。
// 工作中如果使用Lock,一定先编写整体(try...finally块),再编写局部(try块里面的内容)
重入锁功能详解: https://www.cnblogs.com/takumicx/p/9338983.html
读写锁
由读锁(如果你为多个方法加了读锁,操作多个读方法的线程可以同时进入临界区)和写锁(如果多个方法加上了写锁,最多只能有一个线程进入临界区)组成。
读:并行,一旦操作共享资源多个线程的读方法进入临界区,所有的操作写方法的线程必须在外面等待
写:串行,一旦某个线程进入了贡献在资源的写方法临界区,所有的读方法在外面等待,其他的操作写方法的线程也将在外面等待。
一般而言:只是访问共享资源的数据添加读锁,改变共享资源的数据添加写锁。
一旦某个读方法进入临界区,其他的读方法也可以进入,所有的写方法在临界区外面(锁池)等待
一旦某个写方法进入临界区,其他的写方法在临界区外等待,所有的读方法也在临界区外面等待
读写锁效率高于重入锁(Lock)
wait() notify() notifyAll():必须配合synchronized或者Lock一起使用,如果没有synchronized或者Lock会在程序运行时抛出IllegalMonitorStateException。因为wait()不能多个线程同时进入,一旦多个线程同时进入wait()就会出现死锁的可能,JVM会抛出IllegalMonitorStateException。
阻塞状态:wait()/notify()/notifyAll() sleep() join()
死锁:所有的线程都进入了wait() 阻塞状态,无法唤醒对方
正确情况:生产者唤醒消费者,消费者也能唤醒生产者
常见面试题:
解释线程的死锁?工作中你遇到过线程死锁码?什么情况会发生线程死锁
IllegalMonitorStateException是什么异常?工作中有没有遇到过该异常?如何避免该异常
sleep()方法和wait()方法区别?
sleep()方法不会释放共享资源的锁,wait()会释放共享资源的锁
Lock如何代替synchronized?
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 店员是一个共享资源 :可以服务生产者和消费者
* 店员后面有一个餐台:只能放一个产品 -1 表示餐台空 其他表示餐台非空
*/
public class Clerk {
/**
* 默认餐台为空:生产者开始生产,消费者等待
*/
private int product = -1;
private Lock lck = new ReentrantLock(true);
/**
* 锁的条件
* condition对象必须配合lck一起使用
*/
private Condition condition = lck.newCondition();
// wait --->condition.await();
// notify---->condition.signal();
// notifyAll---->condition.signalAll();
/**
* 生产者调用方法
* product:表示生产者生产的产品
*/
public void setProduct(int product)throws Exception{
lck.lock();
try {
// 条件成立:餐台非空生产者等待
while(this.product!=-1) {
condition.await();
}
// 生产者开始生产产品
this.product = product;
// 生产完毕,通知(唤醒)消费者线程,拿走产品
condition.signalAll();
System.out.println("生产者线程生产了第"+this.product+"个产品...");
} finally {
lck.unlock();
}
}
/**
* 消费者调用的方法
* @throws Exception
* IllegalMonitorStateException: wait() notifyAll()必须配合synchronized一起使用
* 否则就会出现上面的异常
*/
public int getProduct()throws Exception{
lck.lock();
try {
//条件成立:表示餐台为空,消费者等待
while(this.product==-1) {
condition.await();
}
//消费者取走产品
int tmp = this.product;
//将餐台设置为空
this.product =-1;
//通知(唤醒)生产者开始生产
condition.signalAll();
System.out.println("消费者线程拿走了第"+tmp+"个产品...");
return tmp;
} finally {
lck.unlock();
}
}
}
无界队列:LinkedBlockingQueue
基于链表结构的阻塞式队列,也就是说它是建立在LinkedList基础上的
Blocking 阻塞:当集合中的元素已经放满了,还继续往里面添加就会阻塞
当集合中的元素已经空了,还继续获取元素也会阻塞
特征:FIFO First-In-First-Out 先进先出
无界限:容量可以是Integer的最大值 2^31-1
import java.util.concurrent.LinkedBlockingQueue;
public class TestLinkedBlockQueue {
public static void main(String[] args) {
// 队列最大容量为3
LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<>(5);
queue.add(1);
queue.add(10);
queue.add(100);
// 超过最大容量,还在网里面添加元素就会抛出IllegalStateException: Queue full
// queue.add(1000);
try {
// 队列元素已经到达上限(满了),还往里面放元素就会阻塞,InterruptedException在阻塞的过程中可能会发生中断异常
queue.put(1000);
System.out.println(queue.size());
// 获取元素首部元素 结果:1
Integer num = queue.peek();
System.out.println(num);
// size:4 仅仅获取元素没有删除(弹出)元素
// System.out.println(queue.size());
// 删除队列中的首部元素并返回删除(弹出)的结果
// Integer num2 =queue.poll();
// 队列中只有3个元素,循环执行poll()方法5次,既没有抛出异常,也没有阻塞,返回null
// for(int i=0;i<5;i++) {
// Integer num3=queue.poll();
// System.out.println(num3);
// }
// System.out.println(queue.size());
// take():获取队列首部元素,并将首部元素弹出(删除),一旦队列中没有元素了还在调用take()方法获取元素,程序就会阻塞
for (int i = 0; i < 5; i++) {
// 循环体执行第5次会阻塞
Integer num6 = queue.take();
System.out.println(num6);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
往队列添加元素
add:队列满了会抛出IllegalStateException异常
offer:队列满了,拒绝处理(新元素无法添加到集合中),不会抛出异常也不会阻塞
成功添加返回ture ,添加失败返回false
put:队列满了程序进入阻塞状态,不会抛出异常
向队列获取元素
peek:会获取首部元素,不会弹出(删除)元素
poll:获取首部元素,并弹出(删除)元素
take:获取首部元素,并弹出(删除)元素 , 如果队列为empty,程序进入阻塞
线程常见面试题
线程和进程有什么区别?
线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。每个线程都拥有单独的栈内存用来存储本地数据。
start() 和 run() 方法有什么区别?
start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程。
线程优先级的级别有哪些?
高优先级10 、低优先级1、普通优先级5(默认)。
笔记过于简单,如需多多学习,详见:Java线程详解