多线程与高并发(一)

Posted by Steven on 2020-09-09
Estimated Reading Time 15 Minutes
Words 3.9k In Total
Viewed Times

1、单核CPU设定多线程是否有意义?
当然后,线程中又不一定全部都要消耗CPU,比如一个线程中有一个HTTP请求,那么在请求等待响应的这段时间就不需要消耗CPU

2、工作线程数是不是设置的越大越好?

不是,线程切换也需要消耗CPU资源

3、工作线程数(线程池中的线程数量)设多少合适?

image-20210527110927017

可以通过工具来测算profiler工具,比如Java中的jprofiler

多线程-基本概念

1、基本概念

程序: wechat.exe 可执行文件

进程: wechat.exe启动后,叫做一个进程,是相对于程序来说的,是个动态的概念 – 操作系统进行资源分配 的基本单位

线程: 作为一个进程里面最小的执行单元就叫一个线程,或者说,一个程序里不同的执行路径就叫做一个线程 – 调度执行 的基本单位

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
package com.steven.shi.javastudy.server;

public class ThreadDemo {

private static class Th1 extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Th1");
}
}
}

public static void main(String[] args) {
//new Th1().run(); //方法调用;如果调用run方法,那么会先去执行run方法,等执行完之后,再执行下面的主程序
new Th1().start(); //调用此方法,下面的主程序继续运行,于此同时,run方法同时运行(这就是不同的线程;即一个程序里,不同的执行路径即为一个线程)
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main");
}
}
}

//运行结果:我们发现Th1和main交替打印
main
Th1
main
Th1
main
Th1
main
Th1
main
Th1

2、创建线程的两种方式

a、继承Thread,重写run方法
1
2
3
4
5
6
7
private static class Th1 extends Thread {
@Override
public void run() {
//TODO:
}
}

b、定义一个类,实现Runnable接口,重写run方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static class Th2 implements Runnable {

/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see Thread#run()
*/
@Override
public void run() {

}
}

3、启动线程的5种方式(基于以上创建的线程)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    //1、从Thread继承
new Th1().start();

//2、实现Runnable接口
new Thread(new Th2()).start();

//3、lambda
new Thread(() -> {
System.out.println("");
}).start();

//4、FutureTask +Callable
//有返回值 通过范型指定
Thread t = new Thread(new FutureTask<String>(new MyCall()));
t.start();

//5、通过线程池来启动
ExecutorService service = Executors.newCachedThreadPool();
service.execute(() -> {
System.out.printf("");
});
service.shutdown();
面试题:

请你告诉我启动线程的三种方式?

1、从Thread继承:new Thread().start();

2、实现Runnable接口:new Thread(Runnable).start();

3、第三种要说线程池,其实也是上面的两种实现的,你可以说通过线程池启动

Executors.neCachedThreadPool()或者FutureTask+Callble

4、线程的几个方法

  • a、 Sleep:睡眠 :当前线程暂停后,就会释放CPU资源,让给其他线程去运行

    CPU一直是从内存中获取指令,运行,没有指令了,就不运行了

    如何唤醒?由设置的睡眠时间决定,等到了设置的时间自动唤醒。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    private static class Th1 extends Thread {
    @Override
    public void run() {
    for (int i = 0; i < 10; i++) {
    try {
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }
    }
  • b、 Yield :非常谦让的让出一下CPU
    当前线程正在执行的时候停下来 进入等待队列 ,让出CPU一下,调度算法会拿等待的线程执行,当然也有可能拿刚刚加入等待队列中的线程继续执行

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {

new Thread(() -> {
for (int i = 0; i < 10; i++) {
if (i % 10 == 0) {
Thread.yield();
}
}
}).start();
}
  • Join :加入,在自己当前的线程中加入其它线程,当前线程等待,等调用的线程运行完了,自己再去执行;如下。th3在运行的时候,join了th1,那么th3等待th1执行完成之后,再去执行
    注意:在th3内部join自己是没有意义的;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Thread th1 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});

Thread th3 = new Thread(() -> {
try {
th1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
面试题:

怎么才能保证三个线程能顺序进行?

运行起来之后,在主线程中,先调用t1.join(),再调用t2.join();,再调用t3.join()

或者直接在t1里面调用t2.join();t2里面调用t3.join();

5、线程的状态

可以通过 t.getState()获取到线程的状态

先来看一张 线程状态迁移图

image-20210527113912854

  1. 新建状态: 当new一个线程的时候,并没有调用start方法的时候,该线程处于新建状态;

  2. Runnable: 线程对象调用start方法,会被线程调度器执行,即交给操作系统执行,操作系统在执行的时候,这个状态叫做Runnable

    • Ready就绪状态: 放到CPU的等待队列等待CPU执行;
    • Running运行状态: 被CPU执行时的状态;

    注意:调用Yiled的时候,线程会从Running状态变成Ready状态,等到线程调度器拿到并且执行的时候又会从Ready变成Running状态

  3. Teminated结束状态: 线程执行完了就会变成这个状态,需要注意的是,线程执行完了之后,就不能再调用start方法了;

  4. TimedWaiting等待状态: (即按照时长等待,过了时长就又回到了等待队列)Thread.sleep(time)wait(time)join(time)LockSupport.parkNanos()LockSupport.parkUntil()

  5. Waiting等待状态: 运行的时候调用了wait()join() LockSupport.park()则会进去Waiting状态;对应的,调用了notify()notifiAll()LockSupport.unpark就会回到Running状态;

  6. Blocked阻塞状态: 比如加了锁synchronized,在同步的代码中没有获得锁就会阻塞状态,获得锁之后就会到等待队列,等到CPU执行

其中,4、5、6三个状态为Runnable中的变迁状态;

**问题:**上面这些状态,哪些是JVM管理的,哪些是操作系统管理的?

上面的状态都是有JVM管理的,因为JVM管理的时候也要通过操作系统。操作系统和JVM是分不开的;JVM是操作系统上的一个程序

**问题:**线程什么状态的时候是被挂起的,挂起是否是一个状态?

在一个CPU上会跑好多个线程,线程从Running状态回到Ready状态叫做被挂起

注意:不要去关闭线程stop(),关闭线程就是要让一个线程正常结束,而不是使用stop() 去结束线程,这样容易导致状态不一致。

面试题:

线程有几种状态:

线程有五种状态:创建、就绪、运行、阻塞、死亡五种状态

线程同步 synchronized关键字

多线程去访问同一个资源的时候需要对这个资源上锁,任何线程需要访问资源的时候,必须先拿到锁;举个很简单的例子就很容易理解为什么需要锁:两个线程同时读取并操作同一个变量,其中一个线程讲内存中的变量读到自己的内存中,然后修改了该变量的值(+1),另一个线程也同时执行这个操作,最终的正确结果应该是2,但是出来的结果是1

注意:这把锁并不是对数字进行上锁,你可以任意指定;比如,你如果想操作变量i,可以先拿到一个对象O的锁,而不一定是对数字加锁;

synchronized的特性

如果说每次定义一个锁的对象Object o ,都需要new出来,那么加锁的时候太麻烦,所以有一个简单的方式就是synchronized(this)所得当前对象,或者在方法上添加synchronized

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.steven.shi.javastudy.server;

public class T {
public static int count = 10;

public synchronized static void test() {
count--;
}

public void test1() {
synchronized (this) {
count--;
}
}
//以上两个方法是等值的
}

我们知道静态方法static是没有this对象的,所以不需要new 出来一个对象就可以执行这个方法,但是如果这个上面加synchronized的话,就代表synchronized(T.class),这里synchronized(T.class)锁的就是T这个对象

看一段程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class T implements Runnable {
public /*volatile*/ int count = 10;

@Override
public /*synchronized*/ void run() {
count--;
System.out.println(Thread.currentThread().getName() + " count=" + count);
}

public static void main(String[] args) {
T t = new T();
for (int i = 0; i < 100; i++) {
new Thread(t, "thread" + i).start();
}
}
}

以上程序在运行后,就会出现下面这个,很容易理解

1
2
3
4
5
6
thread0 count=9
thread2 count=7
thread1 count=8
thread3 count=6
thread4 count=5
........

有两种方法可以解决上面存在的问题

1、使用synchronizedrun方法上

2、count使用volatile关键字

加了synchronized就没有必要加volatile了,因为synchronized既保证了原子性,又保证了可见性

**问题:**T.class是单例的嘛?

如果是在同一个ClassLoader空间那它一定是单例的,不是同一个类加载器就不是了,不同的类加载器互相之间也是不能访问的。如果你能访问它,那么一定是单例的

问题:同步和非同步方法是否可以同时调用?

肯定可以,同步方法又没有加锁,肯定可以访问

脏读

对业务的写方法加锁,对业务的读方法不加锁,这样行不行?

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
public class BankAccount {
private String name;
private BigDecimal balance = new BigDecimal("0.00");

public synchronized void set(String name, BigDecimal balance) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.balance = balance;
}

public /*synchronized*/ BigDecimal get(String name) {
return this.balance;
}

public static void main(String[] args) {
BankAccount bankAccount = new BankAccount();

new Thread(() -> {
bankAccount.set("张三", new BigDecimal(1000));
}).start();

try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(bankAccount.get("张三"));

try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(bankAccount.get("张三"));
}
}

//运行结果:
0.00
1000

上面就是简单的模拟银行账户,然后就会产生脏读的情况,解决的办法就是给get加上synchronized就可以了,如果业务允许脏读,就可以不用加,因为加锁会影响效率(效率低100倍)

可重入锁 :

一个同步方法,可以调用另外一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁

也就是说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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public class ExceptionThread {
public synchronized void exec() {
int count = 0;
System.out.println(Thread.currentThread().getName() + " start");
while (true) {
count++;
System.out.println(Thread.currentThread().getName() + " count =" + count);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (count == 5) {
int i = 1 / 0; //此处抛出异常,锁将被释放,要想不被释放,可以在这里进行catch,然后让循环继续
System.out.println(i);
}
}

}

public static void main(String[] args) {
ExceptionThread t = new ExceptionThread();

Runnable r = new Runnable() {
@Override
public void run() {
t.exec();
}
};
new Thread(r, "t1").start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(r, "t2").start();
}
}

//执行结果,这里是在主方法中,所以程序会顺序执行,等到线程1执行完才会执行线程2,但是线程一中的代码又是个死循环(理想状态下),所以会一直执行,永远不会开始线程2,但是我们运行的时候发现,由于线程1的异常,导致线程2开始运行了,所以就有问题了

t1 start
t1 count =1
t1 count =2
t1 count =3
t1 count =4
t1 count =5
Exception in thread "t1" java.lang.ArithmeticException: / by zero
at com.steven.shi.javastudy.server.ExceptionThread.exec(ExceptionThread.java:19)
at com.steven.shi.javastudy.server.ExceptionThread$1.run(ExceptionThread.java:32)
at java.lang.Thread.run(Thread.java:748)
t2 start
t2 count =1
t2 count =2
t2 count =3
t2 count =4
t2 count =5
Exception in thread "t2" java.lang.ArithmeticException: / by zero
at com.steven.shi.javastudy.server.ExceptionThread.exec(ExceptionThread.java:19)
at com.steven.shi.javastudy.server.ExceptionThread$1.run(ExceptionThread.java:32)
at java.lang.Thread.run(Thread.java:748)

所以在并发处理的过程中,有异常的话要非常小心,不然可能会发生不一致的情况。

比如:在一个web app处理过程中,多个servlet线程共同访问同一个资源,这时候如果异常处理不合适,在第一个线程中抛出异常,其它线程就会进去同步代码区,有可能会访问到异常产生时的数据。

Synchronized 的底层实现
  1. JDK早期的时候,这个synchronized的底层实现是重量级的。重到synchronized都是要去操作系统去申请锁的地步。这样导致sycnhronized的效率非常低

  2. 改进,后来的改进才有了 锁升级 的概念

    参考没错,我就是厕所所长一没错,我就是厕所所长二

锁升级的概念:

当我们使用synchronized的时候,hotspot的实现是这样的,第一个去访问某把锁的线程,比如sync(object),来了之后现在这把锁的头上面markword记录这个线程(如果只是第一个线程访问的时候实际上是没有给这个Object加锁的,在内部实现的时候,只是记录这个线程的Id(偏向锁 ))

偏向锁如果有线程争用的话,就升级为自旋锁 ,(参考上面的文章中的说法就是:有个哥们在蹲马桶,另外来了一个哥们,他就咋旁边等着,他不会跑到CPU的就绪队列中去,而就在这等着占用CPU,用一个while的循环在这转圈,很多圈之后还是不行的话,就再一次进行升级)

自旋锁转圈十次之后,升级为重量级锁 ,重量级锁就去操作系统那里去申请资源,这是一个锁升级的过程

什么情况下使用 自旋锁 比较好?

自旋锁占CPU,但是不访问操作系统,所以在用户态解决锁的问题,不经过内核态,所以效率上要比经过内核态的系统锁效果高

1、执行时间较长的使用系统锁

2、执行时间短、且线程不是很多的时候使用自旋锁

参考文章 深入并发-Synchronized

注意:

1、并不是CAS(Compare and Swap,即比较再交换。)的效率不一定比系统锁要高,这个需要区分实际情况

执行时间短(加锁代码),线程数少,用自旋

执行时间长,线程数多,用系统锁(重量级锁)(synchronized)

2、锁只能升级,不能降级

3、synchronized(Object):这里的Object不能是String 常量、Integer(内部做了一些处理,只要一变化,就会生成新的对象)、Long,最好这些类型就不要用

​ 所有用到String的都是用的同一个,这是一个Library类库,你锁定了一个字符串常量,其他人也尝试锁定这个字符串 常量,即不同人写的代码锁定的是同一个对象,这样就会出问题


如果您喜欢此博客或发现它对您有用,则欢迎对此发表评论。 也欢迎您共享此博客,以便更多人可以参与。 如果博客中使用的图像侵犯了您的版权,请与作者联系以将其删除。 谢谢 !