java基础(十二)

这次记录的是多线程,我们能一遍听歌一遍聊QQ等等,都是多线程的操作。

我们常常都听到进程与线程,而它们却是有差异的。
当我们打开一个应用程序,CPU会给它分配一定的资源,这就是进程。最明显的就是我们打开任务管理器,那些CPU给的内存资源。

而线程呢,我们打开这些应用程序,程序会给我们不停的画窗口,这就是一个进程,我们每打开一个功能,就是打开一个进程。这些进程同时执行,就是多线程的概念。

一个进程至少有一个线程。

并发与并行:
并发是多个处理器,在同一时间,一起执行不同的线程。
并行是一个处理器,在不同时间处理不同的线程。

而我们的处理器大部分都是并行处理,因为有很多线程一起执行。我们往往感受不到卡顿,是因为它执行的速度太快了。这段好啰嗦

多线程的状态

线程的生命周期:新建状态(New)、可运行状态(Runnable)、运行状态(Running)、阻塞状态(Blocked)和死亡状态(Dead)。

多线程的声明和状态情况

下面代码虽长,但我们实际使用的是:

1
2
3
MyThread mt = new MyThread();	
mt.start();
还有重写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
26
27
28
29
30
31
32
33
public class Threading {
public static void main(String[] args) {
MyThread mt = new MyThread(); //新建状态,先New一个线程
System.out.println("线程的状态-新建:"+mt.getState());
//mt.setName("mt"); //线程的取名,一定要在start()执行前调用,我忘了用了
mt.start(); //可运行状态,什么时候运行,看CPU的调度
System.out.println("线程的状态-可运行:"+mt.getState()); //getState()获取线程运行状态

for(int i=0; i<100; i++) {
//Thread.currentThread().getName()获取线程的名称,main也是一个线程
System.out.println(Thread.currentThread().getName()+"使用");
System.out.println("线程的状态-可运行/阻塞:"+mt.getState());
}

try {
Thread.sleep(100); //使用线程休眠100毫秒,这里休眠的是main方法执行的线程
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println("线程的状态-死亡:"+mt.getState());
}
}

class MyThread extends Thread{ //继承Thread类,也就是多线程类
@Override
public void run() { //把要多线程运行的代码贴进去
for(int i=0; i<100; i++) {
System.out.println(this.getName()+"线程"+i+"使用"); //getName()获取当前线程的名字
System.out.println("线程的状态-可运行:"+this.getState());
}
}
}
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
线程的状态-新建:NEW
线程的状态-可运行:RUNNABLE
main使用
线程的状态-可运行/阻塞:RUNNABLE
main使用
线程的状态-可运行/阻塞:RUNNABLE
main使用
线程的状态-可运行/阻塞:RUNNABLE
...
main使用
线程的状态-可运行/阻塞:RUNNABLE
Thread-0线程0使用
main使用
线程的状态-可运行:RUNNABLE
线程的状态-可运行/阻塞:BLOCKED
Thread-0线程1使用
main使用
线程的状态-可运行/阻塞:BLOCKED
main使用
线程的状态-可运行/阻塞:BLOCKED
...
Thread-0线程98使用
线程的状态-可运行:RUNNABLE
Thread-0线程99使用
线程的状态-可运行:RUNNABLE
线程的状态-死亡:TERMINATED

上面则是截取的一段结果,我们可以看到它们执行顺序的不同,这是CPU决定的,我们不敢说也不敢动。

也能看到线程的四个状态。Running是获取不到的,就像我们看得到水,却看不见水流

这是从继承的角度去调用,我们都知道,Java是单继承,所以不能继承其它类。所以我们可以继承多线程的接口——Runnable。它里面只要一个抽象方法run()。所以在实际使用中,还是要传给Thread类里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Threading {
public static void main(String[] args) {
MyThread mt = new MyThread();

Thread t = new Thread(mt,"t"); //第一个传入的是线程的参数,第二个是它的别名
t.start();
}
}

class MyThread implements Runnable{ //继承接口,实现它里面唯一的方法
@Override
public void run() {
for(int i=0; i<100; i++) {
//获取线程名字和状态都是Thread里的,所以这里不能用this
System.out.println(Thread.currentThread().getName()+"线程"+i+"使用");
}
}
}

这里我删除了多余代码,当然运行结果是不一样的,这里相当于是单线程操作,只运行了run()里的方法。

所以多线程的实现方法一般有两种。直接继承Thread类。或者实现Runnable接口,在传值给它的子类Thread类里,这种为了能实现其它类。

多线程的阻塞

多线程的阻塞有四个方法。第一个是刚才写的sleep():它能使指定的线程休眠一段时间。第二个是yield():是指定的线程回到可运行状态等待CPU的下次调用。第三个是join():它是停止之前的线程,让CPU执行现在的线程。第四个是wait()和notify():wait使线程阻塞,notify使可运行。

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 Threading {
public static void main(String[] args) {
MyThread mt = new MyThread();
Thread t1 = new Thread(mt,"t1"); //在这里弄了两个线程一起执行
Thread t2 = new Thread(mt,"t2");
t1.start();
t2.start();
}
}

class MyThread implements Runnable{
public void run() {
for(int i=0; i<10; i++) {
//如果线程的名叫t1,并且循环执行到了第五次,那么t1进入休息10毫秒
if(Thread.currentThread().getName().equals("t1") && i==5) {
try {
Thread.currentThread().sleep(100); //sleep()会返回一个异常,我们在run()方法里,只能try/catch
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
t1:0
t2:0
t1:1
t2:1
t1:2
t1:3
t2:2
t1:4
t2:3
t2:4
t2:5
t2:6
t2:7
t2:8
t2:9
t1:5
t1:6
t1:7
t1:8
t1:9

在执行的前8行运行结果都是无顺序的,而在t1=5时,它休眠了100毫秒,之后就是t2开始执行100毫秒,当然这100毫秒t2已经执行完毕了。100毫秒后就是t1从5开始执行。

yield()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Threading {
public static void main(String[] args) {
MyThread mt = new MyThread();
Thread t1 = new Thread(mt,"t1");
Thread t2 = new Thread(mt,"t2");
t1.start();
t2.start();
}
}

class MyThread implements Runnable{
public void run() {
for(int i=0; i<10; i++) {
if(Thread.currentThread().getName().equals("t1") && i==5) {
Thread.currentThread().yield(); //把这里改了下
}
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}

结果和sleep()类似。当然,因为这里的数据太小,不过它们的思想是不同的,sleep是一直在线程等待,而yield是中断现在的线程,回到可运行状态,等待CPU的再次调用继续执行。

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
29
30
31
32
public class Threading {
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.setName("mt");
mt.start();
}
}

class MyThread extends Thread{
public void run() {
for(int i=0; i<15; i++) {
if(i==5) {
MyThread2 mt2 = new MyThread2();
mt2.setName("mt2");
mt2.start();
try {
mt2.join(); //当mt执行了4次后,mt2启动并执行。
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
class MyThread2 extends Thread{
public void run() {
for(int i=0; i<10; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
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
mt:0
mt:1
mt:2
mt:3
mt:4
mt2:0
mt2:1
mt2:2
mt2:3
mt2:4
mt2:5
mt2:6
mt2:7
mt2:8
mt2:9
mt:5
mt:6
mt:7
mt:8
mt:9
mt:10
mt:11
mt:12
mt:13
mt:14

可以看到,join()的使用,使得mt阻塞,开始执行mt2,等到mt2执行完毕后,才执行mt。join就跟我们平时排队时类似,总有那么几个人插队。

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class Threading {
public static void main(String[] args) {
MyThread mt = new MyThread();
MyThread2 mt2 = new MyThread2();
mt.start();
mt2.start();
}
}

class MyThread extends MyT{
public void run() {
synchronized (o) { //这里是线程同步的方法,也称为线程锁
try {
System.out.println("线程一>>现在等待");
o.wait(); //这里是让o这个对象等待
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程一>>执行完毕");
}
System.out.println("线程一>>结束运行");
}
}
class MyThread2 extends MyT{
public void run() {
try {
Thread.sleep(100);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
synchronized (o) {
try {
Thread.sleep(1000*3); //让当前线程等待3秒
System.out.println("线程二>>休眠状态");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程二>>已被唤醒");
o.notify(); //把o对象唤醒
}
System.out.println("线程二>>结束运行");
}
}
class MyT extends Thread{
static Object o = new Object(); //线程同步要点:保证同步资源一致
}
1
2
3
4
5
6
线程一>>现在等待
线程二>>休眠状态
线程二>>已被唤醒
线程二>>结束运行
线程一>>执行完毕
线程一>>结束运行

如何MyThread2不加Thread.sleep(100),可能会出现下面这样的错误。在MyThread一直等待。所以一定要先让MyThread进入线程锁里。

1
2
3
4
线程二>>休眠状态
线程二>>已被唤醒
线程二>>结束运行
线程一>>现在等待

运行结果以第一个为例。先打印第一行,接着过了大约3秒,打印最后的几行。

为啥要加入synchronized使线程同步呢好像JVM要求使用wait()时,要使用线程同步。而且我们要等待和唤醒的是同一个对象——Object。而wait()不会造成死锁状态,它会把synchronized解锁。

线程的结束

Thread里有两个方法可以结束线程,但都不安全、不可靠。所以,一般都是让它自己运行结束,或使用boolean标记。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Threading {
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();
try {
Thread.sleep(10); //当主线程停止10毫秒,停止mt线程的执行
} catch (InterruptedException e) {
e.printStackTrace();
}
mt.flagToFalse();
}
}

class MyThread extends Thread{
private boolean flag = true;
public void run() {
while(flag) { //用boolean进行标记,如果不改变,那么它将一直执行下去
System.out.println("线程执行");
}
}
public void flagToFalse() {
this.flag = false;
}
}

线程安全

线程为啥不安全

为什么要线程安全呢?我们从下面的例子展开。

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 Threading {
public static void main(String[] args) throws InterruptedException {
MyThread mt = new MyThread();
Thread t1 = new Thread(mt);
Thread t2 = new Thread(mt);
t1.start();
t2.start();
Thread.sleep(1000*15); //15秒后打印结果
mt.printSum();
}
}

class MyThread implements Runnable{
public int sum = 0;
public void run() {
for(int i=0; i<100; i++) {
try {
Thread.currentThread().sleep(100); //模拟网络延迟
} catch (InterruptedException e) {
e.printStackTrace();
}
sum++; //简单模拟求和
}
}
public void printSum() {
System.out.println(sum);
}
}

上面是两个线程启动,有”延迟”的情况下,求sum的和,sum最后的和应该为200。那我们看看实际情况可能为多少。这里只运行了两次,因为要等10多秒。

1
2
193
188

每次得到的答案都不相同,这是为啥?这是两个线程同时运行,在同一时间都拿到了某个数,假设为100,它们都给100赋值,最后sum只加了一次。而取得相同数有很大概率。

下面模拟简单取火车票。

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 Threading {
public static void main(String[] args) throws InterruptedException {
MyThread mt = new MyThread();
Thread t1 = new Thread(mt);
Thread t2 = new Thread(mt);
Thread t3 = new Thread(mt); //三个线程去抢车票
t1.start();
t2.start();
t3.start();
}
}

class MyThread implements Runnable{
public int sum = 20; //假设有20张车票
public void run() {
while(sum>0) {
try {
Thread.currentThread().sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
//模拟取票,看看谁取走哪张票
System.out.println(Thread.currentThread().getName()+">>"+sum--);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Thread-0>>20
Thread-1>>18
Thread-2>>19
Thread-1>>17
Thread-0>>16
Thread-2>>17
Thread-1>>15
Thread-2>>13
Thread-0>>14
Thread-2>>12
Thread-1>>11
Thread-0>>10
Thread-2>>9
Thread-1>>8
Thread-0>>7
Thread-2>>6
Thread-1>>5
Thread-0>>4
Thread-1>>3
Thread-2>>2
Thread-0>>1
Thread-1>>0
Thread-2>>-1

我们可以看到,取了重复的,最后还取了一个负一。重复的如上面所讲,同时取了一个数。而负数呢?还剩最后一个数为1,它们三个都取到了,都进入while循环里,一个取了把sum变成了0,又一个取了把0变为-1 。

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
public class Threading {
public static void main(String[] args) throws InterruptedException {
MyThread mt = new MyThread();
Thread t1 = new Thread(mt);
Thread t2 = new Thread(mt);
Thread t3 = new Thread(mt);
t1.start();
t2.start();
t3.start();
}
}

class MyThread implements Runnable{
public int sum = 20;
public void run() {
synchronized (this) { //添加线程锁,但这锁的是Mythread对象
while(sum>0) {
try {
Thread.currentThread().sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+">>"+sum--);
}
}
}
}

最后也没有出现取重或取多的情况,可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
public class Threading {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new MyThread()); //三个Thread新建三个Mythreadd对象
Thread t2 = new Thread(new MyThread());
Thread t3 = new Thread(new MyThread());
t1.start();
t2.start();
t3.start();
}
}

class MyThread implements Runnable{
public static int sum = 5; //三个不同对象,所以保证它们取的sum都是同一个
public void run() {
synchronized (this) {
while(sum>0) {
try {
Thread.currentThread().sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+">>"+sum--);
}
}
}
}
1
2
3
4
5
6
7
8
9
Thread-2>>5
Thread-1>>4
Thread-0>>4
Thread-0>>3
Thread-2>>2
Thread-1>>3
Thread-2>>1
Thread-0>>0
Thread-1>>1

明明锁了,可还是错了,那是因为我们锁的是对象,三个线程三个对象,对象不同,锁的也不同。那怎么锁?这是我们考虑的。

下面看看,这次锁的是MyThread这个类。

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 Threading {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new MyThread());
Thread t2 = new Thread(new MyThread());
Thread t3 = new Thread(new MyThread());
t1.start();
t2.start();
t3.start();
}
}
class MyThread implements Runnable{
public static int sum = 5;
public void run() {
synchronized (MyThread.class) { //锁的是MyThread类
if(sum<=0) return; //这是简单的优化,很有必要
while(sum>0) {
try {
Thread.currentThread().sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+">>"+sum--);
}
}
}
}

虽然看了三个新的线程,但是结果符合我们的要求,没有重复,没有0和负一。

锁的时候一定要注意锁的是对象还是类,如果是对象是否为同一个对象。

当然,还可以锁run()这个方法,但我不太喜欢用,run()锁的也不是方法,而是这个对象,和我们用锁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
42
43
44
45
46
47
48
49
50
51
52
public class Threading {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new MyThread());
Thread t2 = new Thread(new MyThread2());
t1.start();
t2.start();
}
}
class MyThread implements Runnable{
MyWatch mw = new MyWatch();
MySleep ms = new MySleep();
public void run() {
synchronized (MyWatch.class) { //先锁的是看电视类
mw.myWatch();
try {
Thread.currentThread().sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (MySleep.class) { //锁里在加一个锁,睡觉类
ms.mySleep();
}
}
}
}
class MyThread2 implements Runnable{
MyWatch mw = new MyWatch();
MySleep ms = new MySleep();
public void run() {
synchronized (MySleep.class) { //先锁的睡觉类
ms.mySleep();
try {
Thread.currentThread().sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (MyWatch.class) { //锁里在锁看电视类
mw.myWatch();
}
}
}
}
class MyWatch{
public void myWatch(){
System.out.println(Thread.currentThread().getName()+">>"+"我要看电视");
}
}
class MySleep{
public void mySleep(){
System.out.println(Thread.currentThread().getName()+">>"+"我要睡觉");
}
}
1
2
Thread-0>>我要看电视
Thread-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
class MyThread implements Runnable{
MyWatch mw = new MyWatch();
MySleep ms = new MySleep();
public void run() {
synchronized (MyWatch.class) {
mw.myWatch();
try {
Thread.currentThread().sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized (MySleep.class) {
ms.mySleep();
}
}
}
class MyThread2 implements Runnable{
MyWatch mw = new MyWatch();
MySleep ms = new MySleep();
public void run() {
synchronized (MySleep.class) {
ms.mySleep();
try {
Thread.currentThread().sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized (MyWatch.class) {
mw.myWatch();
}
}
}

线程安全还有Lock()和unlock()方法,一个是上锁,一个是解锁。这里就不弄了,写的太长了….

谢谢您对我的支持
0%