多线程

程序、进程、线程的概念

并行:多个CPU同时执行多个任务。 并发:一个CPU(采用时间片)”同时“执行多个任务。

线程的创建和使用

创建方式一:继承Thread类

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
// 1.创建一个继承于Thread类的子类
class MyThread extends Thread{
// 2.重写Thread类的run()方法->将此线程执行的操作声明在run()中
@Override
public void run() {
for (int i = 0; i < 10; i++) {
if(i%2==0){
System.out.println(i);
}
}
}
}

public class ThreadTest1 {
public static void main(String[] args) {
// 3.在主线程中创建Thread类的子类的对象
MyThread t1 = new MyThread();
// 4.通过此对象调用start()方法:<1>启动当前线程;<2>调用当前线程的run()
t1.start();
// 以下操作在主线程中执行
for (int i = 0; i < 1000; i++) {
if(i%2!=0){
System.out.println(i);
}
}
}
}

子线程的创建和启动过程:
子线程的创建和启动过程

创建方式二:实现Runnable接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 1.创建子类实现Runnable接口
class MyThread2 implements Runnable{
// 2.实现Runnable接口中的run()方法
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+i);
}
}
}
public class ThreadTest2 {
public static void main(String[] args) {
// 3.创建Runnable实现类的对象
MyThread2 mThread = new MyThread2();
// 4.将此对象作为参数传入Thread类的构造器中,创建Thread()类对象
Thread t2 = new Thread(mThread);
// 5.通过Thread类对象调用start()方法
t2.start();

// 再启动一个线程 不需要再造实现类对象
Thread t3 = new Thread(mThread);
t3.start();
}
}

两种创建方式的比较:
开发中:优先选择实现Runnable接口的方式。
原因 :1.没有类的单继承性的局限性;2.更适合处理多个线程有共享数据的情况。
相同点:两种方式都需要重写run(),将线程要执行的逻辑声明在run()中;启动线程都是调用Thread类中的start()方法

Thread类中常用方法

void start(): 启动线程,并执行对象的run()方法
void run():线程在被调度时执行的操作
String getName(): 返回线程的名称
void setName(String name):设置该线程名称
static Thread currentThread(): 返回当前线程。在Thread子类中就是this,通常用于主线程和Runnable实现类
static void yield():线程让步
<1> 暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
<2> 若队列中没有同优先级的线程,忽略此方法
join():在线程a中调用线程b的 join() 方法时,线程a将被阻塞,直到线程b执行完为止 ;低优先级的线程也可以获得执行。底层调用wait(),会释放锁。
static void sleep(long millis):(指定时间:毫秒)
<1> 令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队
<2> 抛出InterruptedException异常
stop():强制线程生命期结束,不推荐使用
boolean isAlive():返回boolean,判断线程是否还活着

线程的调度

调度的策略:时间片;抢占式。
Java的调度方法:
<1> 同优先级线程组成先进先出队列(先到先服务),使用时间片策略;
<2> 对高优先级,使用抢占式策略,高优先级的线程抢占CPU.
线程的优先级等级:
MAX_PRIORITY : 10
MIN_PRIORITY : 1
NORM_PRIORITY : 5
涉及的方法:
getPriority() : 返回线程优先级;
setPriority(int newPriority) : 重新设置线程的优先级。
说明:
线程创建时继承父线程的优先级;
低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用

线程的生命周期

线程生命周期

线程的同步

多个线程执行的不确定性会引起结果的不稳定性;
多个线程对数据的共享,会造成操作的不完整性。
方式一:同步代码块

1
2
3
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
36
37
38
39
40
**
* @author zyz
* @description 测试三个窗口同时卖票,采用同步代码块解决线程安全问题
* @create 2020-02-22 13:57
*/
public class TicketTest1 {
public static void main(String[] args) {
Ticket t = new Ticket();

Thread t1 = new Thread(t);
t1.setName("窗口1");
Thread t2 = new Thread(t);
t2.setName("窗口2");
Thread t3 = new Thread(t);
t3.setName("窗口3");

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

class Ticket implements Runnable {
private int ticketNum = 100;// 采用实现Runnable接口方式实现多线程,共享变量无需声明为static
Object obj = new Object();// 保证多个线程共用一把锁
@Override
public void run() {
while(true){
synchronized(obj){
if(ticketNum>0){
System.out.println(Thread.currentThread().getName()+" 票号:"+ticketNum);
ticketNum--;
}
else{
break;
}
}
}
}
}

方式二:同步方法
1.不需要显式声明同步监视器;
2.非静态的同步方法:同步监视器是this;===>对象锁
静态的同步方法:同步监视器是当前类。===>类锁

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
package multithread;

/**
* @author zyz
* @description 采用同步方法解决线程安全问题
* @create 2020-02-22 16:08
*/
public class TicketTest2 {
public static void main(String[] args) {
Ticket1 t1 = new Ticket1();

Thread t4 = new Thread(t1);
t4.setName("窗口1");
Thread t5 = new Thread(t1);
t5.setName("窗口2");
Thread t6 = new Thread(t1);
t6.setName("窗口3");

t4.start();
t5.start();
t6.start();
}
}

class Ticket1 implements Runnable {
private int ticketNum = 100;// 采用实现Runnable接口方式实现多线程,共享变量无需声明为static
@Override
public void run() {
while(true){
threadRun();
}
}

// 将可能出现线程安全问题的代码块封装成一个同步方法
private synchronized void threadRun(){ // 采用继承Thread类实现多线程时,需要将此方法声明为static
if(ticketNum>0){
System.out.println(Thread.currentThread().getName()+" 票号:"+ticketNum);
ticketNum--;
}
}
}

死锁:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
方式三:Lock
通过显式定义同步锁对象,即Lock对象,来实现同步。
ReentrantLock类实现了Lock,以及创建锁的lock()方法,释放锁的unlock()方法。

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
44
45
46
import java.util.concurrent.locks.ReentrantLock;
/**
* @author zyz
* @description 采用Lock解决线程安全问题
* @create 2020-02-23 14:36
*/
public class TicketTest3 {
public static void main(String[] args) {
Ticket2 t = new Ticket2();

Thread th1 = new Thread(t);
th1.setName("线程1:");
Thread th2 = new Thread(t);
th2.setName("线程2:");
Thread th3 = new Thread(t);
th3.setName("线程3:");

th1.start();
th2.start();
th3.start();
}
}
class Ticket2 implements Runnable{
private int ticketNum = 100;
// 1.实例化ReentrantLock
ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while(true){
try{
// 2.调用锁定方法lock()
lock.lock();
if(ticketNum>0){
System.out.println(Thread.currentThread().getName()+ticketNum);
ticketNum--;
}
else{
break;
}
}finally{
// 3.调用解锁方法unlock()
lock.unlock();
}
}
}
}

synchronized 与 Lock 的不同点:
1.synchronized机制再执行完相应的同步代码后,自动释放同步监视器;
Lock需要手动的启动同步(lock()),同时也需要手动的结束同步(unlock())。
2.Lock只有代码块锁,synchronized有代码块锁和方法锁
3.使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
优先使用顺序:
Lock > 同步代码块(已经进入了方法体,分配了相应资源)>同步方法(在方法体之外)

线程的通信

wait():一旦线程执行此方法,当前线程就会进入阻塞状态,并释放同步监视器(锁)。
notify():一旦执行此方法就会唤醒被wait的一个线程。若有多个线程被wait,则唤醒优先级高的。
notifyAll():唤醒所有被wait的线程。
三个方法必须使用在同步代码块或同步方法中。
三个方法的调用者必须是同步代码块或是同步代码块中的同步监视器。
三个方法定义在Object类中。

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
/**
* @author zyz
* @description 使用两个线程打印1-100。线程1, 线程2 交替打印
* @create 2020-02-23 17:30
*/
public class ThreadCommunicate {
public static void main(String[] args) {
Number n = new Number();

Thread th1 = new Thread(n);
th1.setName("线程1:");
Thread th2 = new Thread(n);
th2.setName("线程2:");
th1.start();
th2.start();
}
}

class Number implements Runnable {
private int num = 1;
@Override
public void run() {
while (true) {
synchronized (this) {
notify();// 唤醒线程
if (num <= 100) {
System.out.println(Thread.currentThread().getName() + num);
num++;
// 线程执行完后就阻塞
try {
wait();// 执行完wait()后释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
}
}
}
}

sleep()与wait()的异同点:
1.相同点:一旦执行后。线程都会进入阻塞状态
2.不同点:
<1>声明的位置不同:sleep()声明在Thread类中,wait()声明在Object类中;
<2>调用要求不同,wait()只能在同步代码块或同步方法中使用;
<3>sleep()不会释放锁。

1.锁池(就绪队列)
所有需要竞争同步锁的线程都会放在锁池当中,比如当前对象的锁已经被其中一个线程得到,则其他线程需要在这个锁池进行等待,当前面的线程释放同步锁后锁池中的线程去竞争同步锁,当某个线程得到后会进入就绪队列进行等待cpu资源分配。
2.等待池(等待队列)
当我们调用wait()方法后,线程会放到等待池当中,等待池的线程是不会去竞争同步锁。只有调用了notify()notifyAll()后等待池的线程才会开始去竞争锁,notify()是随机从等待池选出一个线程放到锁池,而notifyAll()是将等待池的所有线程放到锁池当中。

JDK5.0新增的线程创建方式

方式三:实现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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* @author zyz
* @description 线程创建方式三
* @create 2020-02-26 14:11
*/
public class ThreadTest3 {
public static void main(String[] args) {
// 创建子线程1
// 创建Callable实现类的对象
MyThread3 t = new MyThread3();
// 实例化FutureTask类,将Callable实现类的对象作为参数传入
FutureTask futureTask = new FutureTask(t);
FutureTask futureTask1 = new FutureTask(t);

// Thread(Runnable target)需要传入一个Runnable实现类的对象
// 而FutureTask类不仅实现了Future接口,同时也实现了Runnable接口
// 因此可将FutureTask类的对象作为参数传入
Thread th3 = new Thread(futureTask);
th3.setName("子线程1:");
th3.start();

// get()返回Callable实现类的对象重写的call()的返回值
try {
Object threadName = futureTask.get();
System.out.println(threadName);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
// 创建子线程2
Thread th4 = new Thread(futureTask1);
th4.setName("子线程2:");
th4.start();
}
}
class MyThread3 implements Callable{
@Override
public Object call() throws Exception {
for(int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+i);
}
return Thread.currentThread().getName();
}
}

方式四:使用线程池
好处:
1.提高响应速度(减少了创建新线程的时间)
2.降低资源消耗(重复利用线程池中的线程,不需要每次都创建)
3.便于线程管理:
corePoolSize:核心池的大小
maxinumPoolSize:最大线程数
keepAliveTime:线程没有任务是最多保持多长时间后会终止

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* @author zyz
* @description 线程的创建方式四
* @create 2020-02-26 16:03
*/
public class ThreadTest4 {
public static void main(String[] args) {
// 创建线程池 提供指定线程数量
ExecutorService service = Executors.newFixedThreadPool(10);
// 创建线程并开始线程
service.submit(new MyThread4());
service.execute(new MyThread4());
// 关闭连接池
service.shutdown();
}
}
class MyThread4 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}