线程基础
大家好呀!我是小笙!我学习了韩顺平老师的线程基础的知识,收获颇丰!现在来和大家分享笔记!
线程
进程和线程的概念
概念:进程是指运行中的程序,是程序的一次执行过程或是正在运行的一个程序。动态过程:产生,存在,消亡的过程
那么线程是什么?
线程由进程创建,是进程的一个实体,一个进程可以拥有多个线程
- 单线程:同一个时刻只允许一个线程
- 多线程:同一个时刻,可以执行多个线程
并发:同一个时刻,多个任务交替执行,单核cpu实现多任务
并行:同一个时刻,多个任务同时执行,多核cpu可以实现并行执行多任务
那我们为什么要用多线程而不是多进程呢?
线程间的切换和调度成本远小于进程
线程的生命周期
public enum State {
// 创建进程,但是资源条件未满足
NEW,
// 运行进程
RUNNABLE,
// 阻塞进程
BLOCKED,
// 无时间限制等待notify()方法唤醒
WAITING,
// 有时间限制等待notify()方法唤醒
TIMED_WAITING,
// 结束进程
TERMINATED;
}
线程基本使用
创建线程的两种方式
-
继承Thread类,重写run方法(本质:Thread类也实现了Runable接口)
-
实现Runable接口,重写run方法
// 使用Thread构造接受实现了Runnable的类,可以调用start()方法 public Thread(Runnable target) { this(null, target, "Thread-" + nextThreadNum(), 0); }
源码解析多线程机制
多线程机制说明
用例代码
// 疑问:为什么调用start()方法而不是直接调用run()方法,不都是实现run()方法吗?
// 本质区别有没有创建新的线程,直接调用run方法就是和使用普通方法一样没什么区别,并没有创建线程
public class Thread01 extends Thread{
int times = 0;
public static void main(String[] args) throws InterruptedException {
Thread01 thread01 = new Thread01();
thread01.start();
for (int i = 0; i < 60; i++) {
System.out.println(Thread.currentThread().getName()+i);
Thread.sleep(1000);
}
}
@Override
public void run() {
while(true){
try {
Thread.sleep(1000);
}catch (Exception e){
System.out.println(e.getMessage());
}
System.out.println("喵喵,我是小猫咪"+ ++times );
if(times == 80){
break;
}
}
}
}
使用Terminal – jconsole工具观察进程
注意要main方法和其他进程要持续较长时间。这样子才好观测
源码分析
// 调用线程start方法:thread01.start();
// 源码分析
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
// 本地方法,开辟线程
private native void start0();
进程终止
stop()方法(不推荐)
为什么stop()方法被废弃而不推荐使用呢?
因为stop()方法太过于暴力,强行把执行到一半的程序强行退出,会导致数据不一致的问腿
自制设置标志位退出
public class StopThread {
public static void main(String[] args) throws InterruptedException {
Thread1 thread1 = new Thread1();
thread1.start();
Thread.sleep(10000);
thread1.setFlag(false);
}
}
class Thread1 extends Thread{
private int count = 0;
// 设置标志位来判断线程终止时间
private boolean flag = true;
@Override
public void run() {
while (flag){
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(++count);
}
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
中断方式退出程序
中断方式类似于之前通过标志位方式退出线程的方法,但是中断更加强劲一些,它可以让等待在sleep或者wait的线程引发异常退出
public class InterruptThread {
public static void main(String[] args) throws InterruptedException {
Interrupt thread1 = new Interrupt();
thread1.start();
Thread.sleep(10000);
thread1.interrupt();
}
}
class Interrupt extends Thread{
private int count = 0;
private boolean flag = true;
@Override
public void run() {
while (true){
if(this.isInterrupted()){
System.out.println("Interrupted");
break;
}
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
// 为什么这里还需要中断一次
// 因为sleep()方法中断抛出的异常会清除中断标志位,因此还需要再中断一次
this.interrupt();
}
System.out.println(++count);
}
}
}
线程的常用方法
等待wait()和通知notify()
有些人会好奇,wait和notify方法不是Object类的方法吗,为什么放在线程这里特别拿出来讲?
因为这两个方法平时并不能随便调用,它必须包含在对应的同步块中
public final void wait() throws InterruptedException {
wait(0L);
}
// 当多个线程在等待,则随机通知其中一个等待线程
public final native void notify();
// 通知所有等待线程
public final native void notifyAll();
Object.wait()方法和Thread.sleep()方法的区别
-
wait()方法可以被唤醒,使用wait方法之后会释放目标对象的锁
-
sleep()方法不会释放任何的资源
等待线程结束join()
join()方法:线程的插队,如果插队的线程一旦插入成功,则肯定先执行完插入的线程的所有任务、
// 无线等待,直到目标线程的任务执行完成
public final void join() throws InterruptedException
// 给出一个最大的等待时间
public final synchronized void join(long millis, int nanos) throws InterruptedException
示例代码
public class Join {
public static void main(String[] args) throws InterruptedException {
Thread3 t = new Thread3();
t.start();
for (int i = 1; i <= 20; i++) {
if(i == 5){
System.out.println("让Thread3先完成");
t.join();
System.out.println("main继续执行");
}
System.out.println("main: " + i);
Thread.sleep(1000);
}
}
}
class Thread3 extends Thread{
private int count;
@Override
public void run() {
while(true){
if(count++ == 20){
System.out.println("Thread3结束了");
break;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread3: "+count);
}
}
}
join()方法的底层源码
// join方法的本质就是调用wait方法在当前对象实例进行等待
// 被等待的线程会在执行完成后调用notifyAll()方法唤醒等待的进程进程
public final synchronized void join(final long millis)
throws InterruptedException {
if (millis > 0) {
if (isAlive()) {
final long startTime = System.nanoTime();
long delay = millis;
do {
wait(delay);
} while (isAlive() && (delay = millis -
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)) > 0);
}
} else if (millis == 0)
// 测试此线程是否存在。如果线程已启动且尚未死亡,则该线程处于活动状态 RUNNABLE状态
while (isAlive()) {
wait(0);
}
} else {
throw new IllegalArgumentException("timeout value is negative");
}
}
谦让yeild()
yeild():线程的礼让,让出cpu让其他进程执行,但是礼让的时间不确定,也不一定礼让成功,还有就是当前处理器是否忙碌,如果处理器完成处理的过来,就不会进行礼让
使用场景:当你觉得这个线程不重要或者优先级很低,那适当让出cpu给那些更重要的线程是否是一个明智之举
用户线程和守护线程
用户线程:又称工作线程,当执行的任务执行完或通知方式结束
守护线程:一般是为工作线程服务,当所有线程结束,守护线程自动结束(比如:垃圾回收机制)
public class ThreadMethod {
public static void main(String[] args) {
MyDaemonThread md = new MyDaemonThread();
// 设置为守护线程
md.setDaemon(true);
md.start();
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.getMessage();
}
System.out.println("用户线程在此");
}
}
}
class MyDaemonThread extends Thread{
@Override
public void run() {
while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.getMessage();
}
System.out.println("守护线程在此");
}
}
}
线程同步机制
同步概念:当有多个线程同时在对内存进行操作,在某一个时刻只允许一个线程对该内存进行操作(比如:写操作)
关键字synchronized的作用是实现线程的同步,它的工作是对同步代码枷锁,使得每一次只能有一个线程进入同步块,从而保证了线程的安全
关键字synchronized的用法
// 指定锁对象 默认锁对象就是this
synchronized(对象){} // 需要得到对象的锁,才能操作同步代码
// 直接作用于实例方法 默认锁对象就是this
public synchronized void method(){}
// 直接作用于静态方法 默认锁对象就是当前类.class
public static synchronized void method(){}
public class increase01 implements Runnable{
static int count = 0;
static int count2 = 0;
public static synchronized void increase(){
count++;
}
public synchronized void increase2(){
count2++;
}
public static void main(String[] args) throws InterruptedException {
// 如果同一类传入的对象不同,对象锁就无法启到作用了,必须使用类的锁才可以锁住
Thread t1 = new Thread(new increase01());
Thread t2 = new Thread(new increase01());
t1.start();t2.start();
t1.join(); t2.join();
System.out.println(count); // 20000000
System.out.println(count2); // 小于20000000
i
System.out.println("-------");
increase01.count2 = 0;
increase01.count = 0;
// 传入了相同对象,就不需要使用静态锁,对象锁就可以实现
increase01 inc = new increase01();
Thread thread = new Thread(inc);
Thread thread1 = new Thread(inc);
thread.start(); thread1.start();
thread.join(); thread1.join();
System.out.println(count); // 20000000
System.out.println(count2); // 20000000
}
@Override
public void run() {
for (int i = 0; i < 10000000; i++) {
increase();
}
}
}
释放锁
四种情况释放锁
- 当前线程的同步方法和同步代码块执行结束
- 当前的线程在同步代码块和同步方法中遇到break,return
- 当前线程在同步代码块中出现了未处理的Error或者Exception导致被迫退出
- 当前的线程在同步代码块或者同步方法中执行了wait房啊,暂停当前的线程同时释放资源
二种情况不释放锁
- 线程执行同步代码块或者同步方法时,程序调用Thread.sleep()和Thread.yield()方法不会释放锁
- 线程执行同步代码块或者同步方法时,其他线程调用suspend方法将它挂起,此时它并不会释放该锁(不推荐使用挂起方法)
编程题
1.在main方法中启动两个线程,在第一个线程循环随机打印100 以内的整数,直到第二个线程从键盘中读取了‘Q’命令就终止了第一个线程
public class Homework01 {
public static void main(String[] args) throws InterruptedException {
RandomNum randomNum = new RandomNum();
Thread thread = new Thread(randomNum);
Thread thread2 = new Thread(new Input(randomNum));
thread.start();
thread2.start();
}
}
// 线程1
class RandomNum implements Runnable{
private boolean loop = true;
public void setLoop(boolean loop) {
this.loop = loop;
}
@Override
public void run() {
while(loop){
System.out.println((int)(Math.random()*100));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.getMessage();
}
}
System.out.println("RandomNum退出程序");
}
}
// 线程2
class Input implements Runnable{
private RandomNum r;
public Input(RandomNum r) {
this.r = r;
}
@Override
public void run() {
Scanner sc = new Scanner(System.in);
while(true){
char c = sc.nextLine().charAt(0);
if(c == 'Q'){
r.setLoop(false);
}
System.out.println(c);
if()
}
}
}
2.有两个用户分别从同一个卡上取钱(总金额10000).每次只能取1000元,当金额不足够时,就不能取款了,不能出现超额取款
public class Homework02 {
public static void main(String[] args) {
// 同一个对象 指代的是从同一个卡上取款
withdrawals withdrawals = new withdrawals();
// 不用线程指代的是不用用户取款
Thread user1 = new Thread(withdrawals);
Thread user2 = new Thread(withdrawals);
user1.start();
user2.start();
}
}
class withdrawals implements Runnable{
static int moneySum = 10000;
@Override
public void run() {
while(true){synchronized(this){
if(moneySum>= 1000){
moneySum -= 1000;
System.out.println(Thread.currentThread().getName() +": 取款1000元 ");
System.out.println("余额剩余: "+moneySum);
}else{
System.out.println("余额不足...");
break;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}}
}
}
相关面试题
1.创建线程有哪几种方式?
创建线程有三种方式,分别是继承Thread类、实现Runnable接口、实现Callable接口。
- 通过继承Thread类来创建并启动线程的步骤如下:
- 定义Thread类的子类,并重写该类的run()方法,该run()方法将作为线程执行体。
- 创建Thread子类的实例,即创建了线程对象。
- 调用线程对象的start()方法来启动该线程。
- 通过实现Runnable接口来创建并启动线程的步骤如下:
- 定义Runnable接口的实现类,并实现该接口的run()方法,该run()方法将作为线程执行体。
- 创建Runnable实现类的实例,并将其作为Thread的target来创建Thread对象,Thread对象为线程对象。
- 调用线程对象的start()方法来启动该线程。
- 通过实现Callable接口来创建并启动线程的步骤如下:
- 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call()方法有返回值。然后再创建Callable实现类的实例。
- 使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
- 使用FutureTask对象作为Thread对象的target创建并启动新线程。
- 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
2.线程是否可以重复启动,会有什么后果?
只能对处于新建状态的线程调用start()方法,否则将引发IllegalThreadStateException异常。
扩展阅读
当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时它和其他的Java对象一样,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体。
当线程对象调用了start()方法之后,该线程处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。至于该线程何时开始运行,取决于JVM里线程调度器的调度。
3. 如何实现线程同步?
-
同步方法
即有synchronized关键字修饰的方法,由于java的每个对象都有一个内置锁,当用此关键字修饰方法时, 内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。需要注意, synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类。
-
同步代码块
即有synchronized关键字修饰的语句块,被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。需值得注意的是,同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
-
ReentrantLock
Java 5新增了一个java.util.concurrent包来支持同步,其中ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。需要注意的是,ReentrantLock还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,因此不推荐使用。
-
volatile
volatile关键字为域变量的访问提供了一种免锁机制,使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,因此每次使用该域就要重新计算,而不是使用寄存器中的值。需要注意的是,volatile不会提供任何原子操作,它也不能用来修饰final类型的变量。
-
原子变量
在java的util.concurrent.atomic包中提供了创建了原子类型变量的工具类,使用该类可以简化线程同步。例如AtomicInteger 表可以用原子方式更新int的值,可用在应用程序中(如以原子方式增加的计数器),但不能用于替换Integer。可扩展Number,允许那些处理机遇数字类的工具和实用工具进行统一访问。
4.说一说Java多线程之间的通信方式
-
wait()、notify()、notifyAll()
如果线程之间采用synchronized来保证线程安全,则可以利用wait()、notify()、notifyAll()来实现线程通信。这三个方法都不是Thread类中所声明的方法,而是Object类中声明的方法。原因是每个对象都拥有锁,所以让当前线程等待某个对象的锁,当然应该通过这个对象来操作。并且因为当前线程可能会等待多个线程的锁,如果通过线程来操作,就非常复杂了。另外,这三个方法都是本地方法,并且被final修饰,无法被重写。
wait()方法可以让当前线程释放对象锁并进入阻塞状态。notify()方法用于唤醒一个正在等待相应对象锁的线程,使其进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。notifyAll()用于唤醒所有正在等待相应对象锁的线程,使它们进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。
每个锁对象都有两个队列,一个是就绪队列,一个是阻塞队列。就绪队列存储了已就绪(将要竞争锁)的线程,阻塞队列存储了被阻塞的线程。当一个阻塞线程被唤醒后,才会进入就绪队列,进而等待CPU的调度。反之,当一个线程被wait后,就会进入阻塞队列,等待被唤醒。
-
await()、signal()、signalAll()
如果线程之间采用Lock来保证线程安全,则可以利用await()、signal()、signalAll()来实现线程通信。这三个方法都是Condition接口中的方法,该接口是在Java 1.5中出现的,它用来替代传统的wait+notify实现线程间的协作,它的使用依赖于 Lock。相比使用wait+notify,使用Condition的await+signal这种方式能够更加安全和高效地实现线程间协作。
Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition() 。 必须要注意的是,Condition 的 await()/signal()/signalAll() 使用都必须在lock保护之内,也就是说,必须在lock.lock()和lock.unlock之间才可以使用。事实上,await()/signal()/signalAll() 与 wait()/notify()/notifyAll()有着天然的对应关系。即:Conditon中的await()对应Object的wait(),Condition中的signal()对应Object的notify(),Condition中的signalAll()对应Object的notifyAll()。
-
BlockingQueue
Java 5提供了一个BlockingQueue接口,虽然BlockingQueue也是Queue的子接口,但它的主要用途并不是作为容器,而是作为线程通信的工具。BlockingQueue具有一个特征:当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则该线程被阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果该队列已空,则该线程被阻塞。
程序的两个线程通过交替向BlockingQueue中放入元素、取出元素,即可很好地控制线程的通信。线程之间需要通信,最经典的场景就是生产者与消费者模型,而BlockingQueue就是针对该模型提供的解决方案。
5.说一说sleep()和wait()的区别
- sleep()是Thread类中的静态方法,而wait()是Object类中的成员方法;
- sleep()可以在任何地方使用,而wait()只能在同步方法或同步代码块中使用;
- sleep()不会释放锁,而wait()会释放锁,并需要通过notify()/notifyAll()重新获取锁。