1、单核CPU设定多线程是否有意义?
当然后,线程中又不一定全部都要消耗CPU,比如一个线程中有一个HTTP请求,那么在请求等待响应的这段时间就不需要消耗CPU
2、工作线程数是不是设置的越大越好?
不是,线程切换也需要消耗CPU资源
3、工作线程数(线程池中的线程数量)设多少合适?
可以通过工具来测算profiler
工具,比如Java中的jprofiler
多线程-基本概念
1、基本概念
程序: wechat.exe 可执行文件
进程: wechat.exe启动后,叫做一个进程,是相对于程序来说的,是个动态的概念 – 操作系统进行资源分配 的基本单位
线程: 作为一个进程里面最小的执行单元就叫一个线程,或者说,一个程序里不同的执行路径就叫做一个线程 – 调度执行 的基本单位
1 | package com.steven.shi.javastudy.server; |
2、创建线程的两种方式
a、继承Thread,重写run方法
1 | private static class Th1 extends Thread { |
b、定义一个类,实现Runnable接口,重写run方法
1 | public static class Th2 implements Runnable { |
3、启动线程的5种方式(基于以上创建的线程)
1 | //1、从Thread继承 |
请你告诉我启动线程的三种方式?
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
12private static class Th1 extends Thread {
public void run() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
} -
b、
Yield
:非常谦让的让出一下CPU
当前线程正在执行的时候停下来 进入等待队列 ,让出CPU一下,调度算法会拿等待的线程执行,当然也有可能拿刚刚加入等待队列中的线程继续执行
1 | public static void main(String[] args) { |
Join
:加入,在自己当前的线程中加入其它线程,当前线程等待,等调用的线程运行完了,自己再去执行;如下。th3在运行的时候,join了th1,那么th3等待th1执行完成之后,再去执行
注意:在th3内部join自己是没有意义的;
1 | Thread th1 = new Thread(() -> { |
怎么才能保证三个线程能顺序进行?
运行起来之后,在主线程中,先调用t1.join()
,再调用t2.join();
,再调用t3.join()
或者直接在t1
里面调用t2.join()
;t2
里面调用t3.join();
5、线程的状态
可以通过 t.getState()
获取到线程的状态
先来看一张 线程状态迁移图 :
-
新建状态: 当new一个线程的时候,并没有调用start方法的时候,该线程处于新建状态;
-
Runnable: 线程对象调用start方法,会被线程调度器执行,即交给操作系统执行,操作系统在执行的时候,这个状态叫做Runnable
- Ready就绪状态: 放到CPU的等待队列等待CPU执行;
- Running运行状态: 被CPU执行时的状态;
注意:调用Yiled的时候,线程会从Running状态变成Ready状态,等到线程调度器拿到并且执行的时候又会从Ready变成Running状态
-
Teminated结束状态: 线程执行完了就会变成这个状态,需要注意的是,线程执行完了之后,就不能再调用start方法了;
-
TimedWaiting等待状态: (即按照时长等待,过了时长就又回到了等待队列)
Thread.sleep(time)
、wait(time)
、join(time)
、LockSupport.parkNanos()
、LockSupport.parkUntil()
等 -
Waiting等待状态: 运行的时候调用了
wait()
、join()
LockSupport.park()
则会进去Waiting状态;对应的,调用了notify()
、notifiAll()
、LockSupport.unpark
就会回到Running状态; -
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 | package com.steven.shi.javastudy.server; |
我们知道静态方法static是没有this对象的,所以不需要new 出来一个对象就可以执行这个方法,但是如果这个上面加synchronized
的话,就代表synchronized(T.class)
,这里synchronized(T.class)
锁的就是T这个对象
看一段程序:
1 | public class T implements Runnable { |
以上程序在运行后,就会出现下面这个,很容易理解
1 | thread0 count=9 |
有两种方法可以解决上面存在的问题
1、使用synchronized
在run
方法上
2、count使用volatile
关键字
加了synchronized
就没有必要加volatile
了,因为synchronized
既保证了原子性,又保证了可见性
**问题:**T.class是单例的嘛?
如果是在同一个ClassLoader空间那它一定是单例的,不是同一个类加载器就不是了,不同的类加载器互相之间也是不能访问的。如果你能访问它,那么一定是单例的
问题:同步和非同步方法是否可以同时调用?
肯定可以,同步方法又没有加锁,肯定可以访问
脏读
对业务的写方法加锁,对业务的读方法不加锁,这样行不行?
1 | public class BankAccount { |
上面就是简单的模拟银行账户,然后就会产生脏读的情况,解决的办法就是给get
加上synchronized
就可以了,如果业务允许脏读,就可以不用加,因为加锁会影响效率(效率低100倍)
可重入锁 :
一个同步方法,可以调用另外一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁
也就是说synchronized获得锁是可重入的,如果是不可重入的话,就会造成死锁
异常锁
程序执行过程中,如果出现异常,默认情况下锁会被释放
1 | public class ExceptionThread { |
所以在并发处理的过程中,有异常的话要非常小心,不然可能会发生不一致的情况。
比如:在一个web app处理过程中,多个servlet线程共同访问同一个资源,这时候如果异常处理不合适,在第一个线程中抛出异常,其它线程就会进去同步代码区,有可能会访问到异常产生时的数据。
Synchronized
的底层实现
-
JDK早期的时候,这个synchronized的底层实现是重量级的。重到synchronized都是要去操作系统去申请锁的地步。这样导致sycnhronized的效率非常低
-
改进,后来的改进才有了 锁升级 的概念
锁升级的概念:
当我们使用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类库,你锁定了一个字符串常量,其他人也尝试锁定这个字符串 常量,即不同人写的代码锁定的是同一个对象,这样就会出问题
如果您喜欢此博客或发现它对您有用,则欢迎对此发表评论。 也欢迎您共享此博客,以便更多人可以参与。 如果博客中使用的图像侵犯了您的版权,请与作者联系以将其删除。 谢谢 !