一.关于多线程

如果要处理多个业务时,而这些业务之间没有很强的依赖,如A,B,C,此时不按顺序执行这三个业务。当A出现阻塞时,此时CPU是空闲的,我们可以先让B加载进CPU进行执行,当A没有被阻塞时再将A让CPU执行,这样效率明显要高于顺序执行。多线程解决的是并发的问题,目的是使任务执行效率更高,实现前提是“阻塞”。它们看上去时同时在执行的,但实际上只是分时间片使用CPU而已。


二.进程与线程

线程是比进程更小一级的执行单位,一个进程可以包含多个线程。多进程操作系统能同时运行多个进程(程序),由于 CPU 具备分时机制,所以每个进程都能循环获得自己的CPU 时间片。由于 CPU 执行速度非常快,使得所有程序好像是在同时运行一样。我们最常见的就是main方法可以看做一个进程或者是一个主线程,而在main方法里面开启的线程就是其子线程,他们同时存在,也可以同时运行。


三.开启线程的方式

1.继承Thread类

定义一个类,继承Thread类,并重写Thread类的run()方法。run()方法是线程要完成的功能。创建一个继承了Thread类的对象,产生一个线程。并使用该对象的start方法,启动线程。

注意:如果只是调用run()方法,只是相当于普通调用了次方法,JVM不会启动线程。必须使用start()方法,JVM自动调用此线程的run()方法

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 myThread  extends Thread{
private String name;
public ThreadOne() {}
public ThreadOne(String name) {
super();
this.name = name;
}
public void run() {
for(int i=0;i<10;i++) {
System.out.println(name + i);
}
}
}

//测试类

public class Test {
public static void main(String[] args) {
myThread t1 = new myThread("第一个线程:");
myThread t2 = new myThread("第二个线程:");
t1.start();
t2.start();
}
}

2.实现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
25
26
27
28
29
30
public class myThread implements Runnable {
private String name;
public ThreadOne() {
super();
}
public ThreadOne(String name) {
super();
this.name = name;
}
@Override
public void run() {
for(int i = 0; i < 10; i++) {
System.out.println(name + i);
}
}
}

//测试类

public class Test {
public static void main(String[] args) {
myThread threadOne = new myThread("第一个线程:");
myThread threadTwo = new myThread("第二个线程:");
Thread t1 = new Thread(threadOne);
Thread t2 = new Thread(threadTwo);
t1.start();
t2.start();
}
}

这两种开启线程的运行结果都是两个子线程(t1,t2)相互争夺CPU资源,可能 t1线程执行了一小半,t2线程开始执行了,而后t1又将CPU资源抢占过去,这些都是随机的,那么有没有能控制线程的执行顺序的方法呢?

  • public void final setPriority(int newPriority)
  • public void final getPriority()

Java为我们提供了两个方法,很显然一个是设置优先级,一个是获取优先级。其中newPriority是优先指数,它越大优先级越高。但是用此方法可能会有异常。

3.异常

a.IllegalArgumentException 如果优先级不在MIN_PRIORITY和MAX_PRIORITY之间就会出此异常
注:MAX_PRIORITY = 10,MIN_PRIORITY = 1,NORM_PRIORITY = 5,线程默认优先级为5

b.SecurityException 如果当前线程不能修改此线程就会出此异常

因此我们给 t1 设置优先级时,会优先将 t1 执行完毕(若优先级相等则随机执行)

1
t1.setPriority(10)//此时优先执行优先级高的,也就是t1

四.线程控制

关于线程控制有三个方法,

  • static void sleep(long millis) 暂停millis毫秒执行此线程
  • void join() 等待这个线程死亡
  • void setDaemon(boolean on) 标记此为守护线程,若所有线程都是守护线程,JVM将退出,但不是立即退出

1.sleep()方法

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 myThread  extends Thread{
public void run() {
for(int i = 0; i < 10; i++) {
System.out.println(getName + ":" + i);
try{
Thread.sleep(100);
} catch(InterruptException e){
e.printStackTrace();
}
}
}
}

//测试类
public class Test {
public static void main(String[] args) {
myThread t1 = new myThread();
myThread t2 = new myThread();
t1.setName("cc")
t2.setName("aa")
t1.start();
t2.start();
}
}

2.join()方法

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
public class myThread  extends Thread{
public void run() {
for(int i=0;i<10;i++) {
System.out.println(getName + ":" + i);
}
}
}

//测试类
public class Test {
public static void main(String[] args) {
myThread t1 = new myThread();
myThread t2 = new myThread();
myThread t3 = new myThread();
t1.setName("aa");
t2.setName("bb");
t3.setName("cc");
try{
t1.join();
} catch(InterruptException e){
e.printStackTrace();
}
t1.start();
t2.start();
t3.start();
}
//只有将t1执行完后才会开启t2和t3线程
}

3.setDaemon()方法

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
public class myThread  extends Thread{
public void run() {
for(int i=0;i<10;i++) {
System.out.println(getName + ":" + i);
}
}
}

//测试类
public class Test {
public static void main(String[] args) {
myThread t1 = new myThread();
myThread t2 = new myThread();
t1.setName("aa");
t2.setName("bb");
Thread.currentThread().setName("c");//设置主线程名字为c
t1.setDaemon(true);
t2.setDaemon(true);
t1.start();
t2.start();
for(int i = 0; i < 10; i++){
System.out.println(Thead.currentThread.getName + ":" + i);
}
}
//若主线程执行完后,JVM将关闭,程序结束
}

五.线程的生命周期

1.java线程周期

2.线程的五个状态

  • 新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();

  • 就绪状态(Runnable):当调用线程对象的start()方法,线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了start()方法后,此线程立即就会执行;

  • 运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;

  • 阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:

  • 死亡状态(Dead):线程执行完了或者因异常退出了run()方法

(1)等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;

(2)同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;

(3)其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。


六.线程安全

当多个线程中有多语句操作同一个数据时,可能会出现数据安全问题,我们以卖票这个经典例子来说明。

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
public class Ticket implements Runnable {
final Object obj = new Object();
int ticket = 100;

@Override
public void run() {
while (true) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + ticket);
ticket--;
}
}
}
}

//测试类
public class Test {
public static void main(String[] args) {
Ticket tt = new Ticket();
Thread t1 = new Thread(tt);
Thread t2 = new Thread(tt);
t1.setName("售票一");
t2.setName("售票二");
t1.start();
t2.start();
}
}

t1和t2共用一个资源,他们都会相互争夺CPU资源,当t1被CPU加载后,因为调用了sleep()方法就会进入阻塞状态,这时t2就会被CPU加载,这就会出现如图所示售票一和售票二交替出现的效果。

那么如何解决这种问题呢?

1.使用同步代码块

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
public class Ticket implements Runnable {
final Object obj = new Object();
int ticket = 100;

@Override
public void run() {
while (true) {
synchronized (obj) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + ticket);
ticket--;
}
}
}
}
}

//测试类
public class Test {
public static void main(String[] args) {
Ticket tt = new Ticket();
Thread t1 = new Thread(tt);
Thread t2 = new Thread(tt);
t1.setName("售票一");
t2.setName("售票二");
t1.start();
t2.start();
}
}

使用synchronized即可,当t1率先抢占到CPU资源时,它就为这个代码块上了锁,当他调用sleep()方法进去阻塞阶段时,t2进入CPU加载,但由于被上了锁,因此t2只能等待t1执行结束,释放了CPU资源时才能被CPU加载,这就保证了数据的安全。

2.使用同步方法

在方法名前面添加关键字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 Ticket implements Runnable {
int ticket = 100;

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

public synchronized void sellTicket() {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + ticket);
ticket--;
}
}
}

//测试类
public class Test {
public static void main(String[] args) {
Ticket tt = new Ticket();
Thread t1 = new Thread(tt);
Thread t2 = new Thread(tt);
t1.setName("售票一");
t2.setName("售票二");
t1.start();
t2.start();
}
}

3.Lock锁

使用同步代码块不能直观的看到锁与解锁,而Lock锁能更清晰的表达如何加锁和释放锁,比synchronized方法可以更加获得广泛的锁定操作。

Lock中的两个方法

  • void lock():获得锁
  • void unlock():释放锁

Lock是接口不能直接实例化,只能采用它的实现类ReentrantLock来实例化。

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
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Ticket implements Runnable {
Lock lock = new ReentrantLock();
int ticket = 100;

@Override
public void run() {
while (true) {
使用try{}catch{}语句防止线程中方法出错导致不能成功释放锁而一直占用CPU资源的情况
try {
lock.lock();
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + ticket);
ticket--;
}
}finally {
lock.unlock();
}
}
}
}

//测试类
public class Test {
public static void main(String[] args) {
Ticket tt = new Ticket();
Thread t1 = new Thread(tt);
Thread t2 = new Thread(tt);
t1.setName("售票一");
t2.setName("售票二");
t1.start();
t2.start();
}
}