Java基础(八)—— 线程

本文最后更新于:2022年3月6日 晚上

概览:Java多线程0

多线程创建的三种方式

创建方式 使用场景
继承Thread 单继承
实现Runnable 无返回值任务
实现Callable 有返回值任务

1 继承Thread

1
2
3
4
5
6
7
8
9
10
11
public class MyThread extends Thread{

@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 线程启动运行");
}

public static void main(String[] args) {
new MyThread().start();
}
}
  • 由于Java只支持单继承,所以一个类一旦继承了Thread类就不能继承其他类了,对于功能扩展有很大的限制,不推荐使用。

2 实现Runnable

1
2
3
4
5
6
7
8
9
10
11
12
public class TestRunnable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 线程启动运行");
}

public static void main(String[] args) {
TestRunnable testRunnable = new TestRunnable();
Thread thread = new Thread(testRunnable);
thread.start();
}
}
  • Runnable仅仅只是一个接口,没有启动线程的能力,需要搭配之Thread类来使用。

lambda写法

1
2
3
new Thread(()->{
System.out.println(Thread.currentThread().getName() + " 线程启动运行");
}).start();
  • 因为Runnable接口是一个函数式接口,可以简写为lambda表达式。

匿名内部类形式

1
2
3
4
5
6
new Thread(new Runnable(){
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 线程启动运行");
}
}).start();

3 实现Callable接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class TestCallable implements Callable<String> {
@Override
public String call() throws Exception {
return Thread.currentThread().getName() + " 线程启动运行";
}

public static void main(String[] args) {
TestCallable testCallable = new TestCallable();
// 创建FutureTask实例
FutureTask<String> futureTask = new FutureTask<>(testCallable);
// 创建线程
Thread thread = new Thread(futureTask);
// 启动线程
thread.start();
try {
// 获取任务执行的结果
String result = futureTask.get();
System.out.println(result);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
  • 需要指明类型,即继承时的泛型类型要指明。
  • Callable仅仅只是一个接口,没有启动线程的能力,需要搭配之Thread类来使用。

线程的基础

currentThread() 获取当前正在执行的线程

1
2
3
Thread.currentThread();//静态方法

System.out.println(Thread.currentThread()); // Thread[main,5,main]
  • mian表示线程名称:getName()
  • 5表示线程的优先级:getPriority()
  • main表示线程所属的线程组名称:group.getName()

run() 与 start() 区别

  • 位置

    • 均位于Thread类,但run()是重写了Runnable接口
  • 类型

    • run()是非同步方法
    • start()是同步方法,多个方法同时执行时不会存在线程安全问题
  • 作用

    • run()存放任务代码
    • start()启动线程,它会自动调用run()方法
  • 线程数量

    • run()不会产生新线程
    • start()会产生一个新线程
  • 调用次数

    • run()可以被调用无数次
    • start()被重复调用多次就会出问题,因为线程不能被重复启动。

    一个线程对象只能调用一次start方法.从new到等待运行是单行道,所以如果你对一个已经启动的线程对象再调用一次start方法的话,会产生:IllegalThreadStateException异常. 可以被重复调用的是run()方法。

线程优先级

  • getPriority()获取到优先级。
  • setPriority(int)设置优先级
  • 有限级等级从1到10,最低的是1级。有三个常量优先级
    • MIN_PRIORITY=1
    • NORM_PRIORITY = 5
    • MAX_PRIORITY = 10最大优先级

线程休眠 sleep()

  • static方法
  • 是的当前线程休眠一段时间,放弃CPU的执行权

线程停止 interrupt()

  • interrupt仅仅是将线程标记为中断状态,并没有实际去停止线程。
  • 如果想要线程停下来,那么要求我们手动检查线程状态,然后对此做出反应。
  • isInterrupted()非静态方法,判断线程是否被中断
  • interrupted(),静态方法,判断线程是否被中断,并清除中断标记
  • 中断休眠中的线程将会导致异常的发生。

使正在执行的线程放弃执行权——yield()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 赋值线程
class ValueTask implements Runnable{
public static int value = 0;
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
value = 100;
}
}

// 打印线程
class PrintTask implements Runnable{

@Override
public void run() {
while(ValueTask.value == 0){
System.out.println("000");
Thread.yield();
}
System.out.println("Value:"+ValueTask.value);
}
}

public class threadlearn {
public static void main(String[] args) {
Thread thread1 = new Thread(new ValueTask());
Thread thread2 = new Thread(new PrintTask());
thread1.start();
thread2.start();
}
}
  • 代码比较
1
2
3
4
5
6
7
8
9
// 1
while(ValueTask.value == 0){
System.out.println("000");
Thread.yield();
}
// 2
while(ValueTask.value == 0){
continue;
}

代码1和代码2的区别是,代码1会放弃执行权,而代码2则是一直空转,直到条件成立。

等待线程死亡 join()

  • join()可以等待线程顺利执行完毕还是中途被中断。

让线程A随着主线程结束而结束

同步

synchronized修饰符

  • 修饰代码块:同步代码块
  • 修饰方法名:同步方法
  • 修饰静态方法:静态同步方法

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class TicketTask implements Runnable {

private int num = 10;

@Override
public void run() {
while (num > 0) {
/* 需要一个对象,放置this */
synchronized (this) {
if (num > 0) {
/* 再一次的判断,放置出现0号票和-1号票 */
System.out.println(num + "号票");
num--;
}
}

}
}
}

public class Main {

public static void main(String[] args) {
TicketTask task1 = new TicketTask();

/* 将一个卖票任务传递给三个线程 */
Thread t1 = new Thread(task1);
Thread t2 = new Thread(task1);
Thread t3 = new Thread(task1);

t1.start();
t2.start();
t3.start();
}
}

同步代码块封装成同步方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class TicketTask implements Runnable {

private int num = 10;

@Override
public void run() {
while (num > 0) {
sellTicket();
}
}

public synchronized void sellTicket(){
if (num > 0) {
/* 再一次的判断,放置出现0号票和-1号票 */
System.out.println(num + "号票");
num--;
}
}
}
  • 一般不要把while循环放入到同步代码块,这意味这可能某个线程就执行完了全部的while循环的代码
  • 同步代码块内外双判断!
    • 例如:单例模式

同步锁:同步锁是为了保证每一个线程都能正常执行原子不可更改操作,同步监听对象、同步锁、同步监听器、互斥锁的一个标记锁。

能够成为锁的类型:

  • 对象类型:即new出来的对象
  • 类类型:Object.class
同步类型 锁类型
同步代码块 对象类型,this,类类型
同步方法 this
静态同步方法 类类型

线程等待唤醒机制

  • wait():使当前线程等待

  • notify():唤醒单个等待的线程,需要当前的锁对象才能够调用

  • notifyAll():唤醒所有等待的线程

  • 以上三者都需要拥有锁之后再去调用,才能等待或者唤醒线程,否则就会抛出异常

wait & sleep

  • wait是锁方法,位于Object内,sleep方法是线程方法
  • wait需要当前线程持有锁,没有拥有锁而调用会使得当前程序发生异常。sleep方法不需要当前线程拥有锁,可以直接调用
  • wait支持手动唤醒,notify()和notifyAll()可以唤醒,而sleep方法不能够被手动唤醒
  • 两个方法都支持自动唤醒,可以设置超时时间
  • 两者都可以支持中断操作,但是调用interrupt()方法之后都会发生InterruptedException线程异常
  • wait方法被调用时会立即释放锁,而sleep方法不会释放锁。
  • wait有两种状态:WAITING和TIMED_WAITING,而sleep方法调用后只有TIMED_WAITING。
    • wait有多个方法,支持手动唤醒(WAITING)和超时唤醒(TIMED_WAITING),

线程间通讯——wait&notify应用

生产者消费者示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class Produce implements Runnable {

private static final Logger log = LoggerFactory.getLogger(Produce.class);

private Data data;

public Produce(Data data) {
this.data = data;
}

@Override
public void run() {
int i = 0;
while (true) {
synchronized (data){
if (data.getMsg() == null) {
log.info("produce# produce a msg:{}", i);
data.setMsg("produce a msg " + i++);
// 唤醒消费者
data.notify();
// 自身等待
try {
data.wait();
} catch (Exception e) {
log.error("err: ", e);
}
}
}
}
}
}

消费者示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class Consumer implements Runnable {

private static final Logger log = LoggerFactory.getLogger(Consumer.class);

// 1 两个不同的线程如何使用同一个锁? 使用同一个对象作为他们的属性
private Data data;

public Consumer(Data data) {
this.data = data;
}

@Override
public void run() {
while (true) {
synchronized (data) {
if (data.getMsg() != null) {
log.info("consumer# consumer a data:{}", data.getMsg());
data.setMsg(null);
// 唤醒生产者
data.notify();
// 自身等待
try {
data.wait();
} catch (Exception e) {
log.error("err:", e);
}
}
}
}
}
}

mian:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class PCMain {

public static void main(String[] args) {

Data d1 = new Data();
Produce produce = new Produce(d1);
Consumer consumer = new Consumer(d1);

Thread t1= new Thread(produce);
Thread t2 = new Thread(consumer);

t1.start();
t2.start();
}
}
  1. 使用同一个对象作为多个不同线程的属性,以此作为他们的共同资源,也可以来充当锁
  2. wait以及notify都需要锁对象来去调用
  3. notify之后再调用wait,这种时序上的原理是什么?

Lock显式锁

  • 可以实现线程同步

  • 可以自由的获取锁与释放锁lock()、unlock()

  • lock()是阻塞式获取锁,即如果当前所被占用,就一直阻塞等待。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class TestLock implements Runnable {
private Lock lock = new ReentrantLock();
@Override
public void run() {
lock.lock();// 获取锁
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "lock");
lock.unlock();//释放锁
}
}

public static void main(String[] args) {
TestLock task1 = new TestLock();

new Thread(task1).start();
new Thread(task1).start();
}
  • tryLock()是非阻塞方式获取锁,即尝试获取锁,如果锁可用,则返回true,否则返回false。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class TestLockTry implements Runnable {
private Lock lock = new ReentrantLock();
@Override
public void run() {
if(lock.tryLock()){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
System.out.println(Thread.currentThread().getName() + "lock");
}else{
System.out.println(Thread.currentThread().getName() + "未获取到锁");
}
}
}

lock常用子类

  • ReentrantLock可重入锁
  • ReentrantReadWriteLock.ReadLock:读锁
  • ReentrantReadWriteLock.WriteLock:写锁

中断正在等待锁的线程

  • synchronized,不能中断等待锁的线程
  • Lock.lock(),不能中断等待锁的线程
  • Lock.lockInterruptibly(),可以中断
1
2
3
4
5
6
7
8
@Override
public void run() {
try {
lock.lockInterruptibly();
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "当前线程被中断!");
}
}

Lock的等待唤醒机制 Condition

await() 使当前线程等待,直到唤醒或被中断为止
awaitUninterruptibly() 使当前线程等待,直到唤醒为止。
awaitNanos(Long nanosTimeout) 使当前线程等待,直到唤醒或被中断或经过指定的等待时间为止。
await(Long time, TimeUnit unit) 使当前线程等待,直到唤醒或被中断或经过指定的等待时间为止。
awaitUntil(Date deadLine) 使当前线程等待,直到唤醒或被中断或经过指定的等待时间为止。
signal() 唤醒个等 待线程。
signaLAlL() 唤醒所有等待的线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class TestCond implements Runnable{

private Lock lock;
private Condition cond;

public TestCond(Lock lock,Condition cond){
this.lock = lock;
this.cond = cond;
}

@Override
public void run() {
// 获取锁
lock.lock();
try {
// 使得当前线程等待
cond.await();
System.out.println(Thread.currentThread().getName());
} catch (InterruptedException e) {
System.out.println("err!");
}finally {
lock.unlock();
}
}
}

public static void main(String[] args) {
Lock lock = new ReentrantLock();
Condition cond = lock.newCondition();

TestCond tc = new TestCond(lock,cond);
Thread t = new Thread(tc);
t.start();

try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.lock();
cond.signal();// 唤醒线程
lock.unlock();
}

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!