多线程简介
在现代计算机中往往存在多个CPU
核心,而1
个CPU
能同时运行一个线程,为了充分利用CPU
多核心,提高CPU
的效率,多线程就应时而生了。
那么多线程就一定比单线程快吗?答案是不一定,因为多线程存在单线程没有的问题
- 上下文切换:线程从运行状态切换到阻塞状态或者等待状态的时候需要将线程的运行状态保存,线程从阻塞状态或者等待状态切换到运行状态的时候需要加载线程上次运行的状态。线程的运行状态从保存到再加载就是一次上下文切换,而上下文切换的开销是非常大的,而我们知道
CPU
给每个线程分配的时间片很短,通常是几十毫秒(ms),那么线程的切换就会很频繁。 - 死锁:死锁的一般场景是,线程
A
和线程B
都在互相等待对方释放锁,死锁会造成系统不可用。 - 资源限制的挑战:资源限制指计算机硬件资源或软件资源限制了多线程的运行速度,例如某个资源的下载速度是
1Mb/s
,资源的服务器带宽只有2Mb/s
,那么开10
个线程下载资源并不会将下载速度提升到10Mb/s
。
既然多线程存在这些问题,那么我们在开发的过程中有必要使用多线程吗?我们知道任何技术都有它存在的理由,总而言之就是多线程利大于弊,只要我们合理使用多线程就能达到事半功倍的效果。
多线程的意思就是多个线程同时工作,那么多线程之间如何协同合作,这也就是我们需要解决的线程通信、线程同步问题
- 线程通信:线程通信指线程之间以何种机制来交换消息,线程之间的通信机制有两种:共享内存和消息传递。共享内存即线程通过对共享变量的读写而达到隐式通信,消息传递即线程通过发送消息给对方显示的进行通信。
- 线程同步:线程同步指不同线程对同一个资源进行操作时候线程应该以什么顺序去操作,线程同步依赖于线程通信,以共享内存方式进行线程通信的线程同步是显式的,以消息传递方式进行线程通信的线程同步是隐式的。
synchronized简介
synchronized
是Java的关键字,可用于同步实例方法、类方法(静态方法)、代码块
- 同步实例方法:当
synchronized
修饰实例方法的时候,同步的范围是当前实例的实例方法。 - 同步类方法:当
synchronized
修饰类方法的时候,同步的范围是当前类的方法。 - 同步代码块:当
synchronized
修饰代码块的时候,同步的范围是()
中的对象。
"talk is cheap show me the code"
让我们分别运行个例子来看看。
- 同步实例方法
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
35synchronized public void synSay() {
System.out.println("synSay----" + Thread.currentThread().getName());
while (true) { //保证进入该方法的线程 一直占用着该同步方法
}
}
public void say() {
System.out.println("say----" + Thread.currentThread().getName());
}
public static void main(String[] args){
Test test1 = new Test();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
test1.synSay();
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000); //休眠3秒钟 保证线程t1先执行
} catch (InterruptedException e) {
e.printStackTrace();
}
test1.say();
test1.synSay();
}
});
t1.start();
t2.start();
}
运行输出1
2synSay----Thread-0 //线程t1
say----Thread-1 //线程t2
创建t1
,t2
两个线程,分别执行同一个实例test1
的方法,线程t1
先执行加了同步关键字的synSay
方法,注意方法里面需要加上个while
死循环,目的是让线程一直在同步方法里面,然后然线程t1执行之后再让线程t2去执行,此时线程t2并不能成功进入到synSay
方法里面,因为此时线程t1正在方法里面,线程2只能在synSay
方法外面阻塞,但是线程t2可以进入到没有加同步关键字的say
方法。
也就是说关键字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
41synchronized static public void synSay() {
System.out.println("static synSay----" + Thread.currentThread().getName());
while (true) { //保证进入该方法的线程 一直占用着该同步方法
}
}
synchronized public void synSay1() {
System.out.println("synSay1----" + Thread.currentThread().getName());
}
public void say() {
System.out.println("say----" + Thread.currentThread().getName());
}
public static void main(String[] args){
Test test1 = new Test();
Test test2 = new Test();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
test1.synSay();
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000); //休眠3秒钟 保证线程t1先执行
} catch (InterruptedException e) {
e.printStackTrace();
}
test1.say();
test2.say();
test1.synSay();
}
});
t1.start();
t2.start();
}
运行输出1
2
3
4
5
6
7
8static synSay----Thread-0 //线程t1 实例test1
say----Thread-1 //线程t2 实例test1
say----Thread-1 //线程t2 实例test2
static synSay----Thread-0 //线程t1 实例test1
say----Thread-1 //线程t2 实例test1
synSay1----Thread-1 //线程t2 实例test1
say----Thread-1 //线程t2 实例test2
这里和上面的同步实例方法的代码差不多,就是将synSay
方法加上了static
修饰符,即把方法从实例方法变成类方法了,然后我们再新建个实例test2
,先让线程t1调用实例test1的synSay类方法,在让线程t2去调用实例test1的say实例方法、synSay类方法和让线程t2去调用实例test2的say实例方法,发现在线程t1占用加了同步关键字的synSay
类方法的时候,别的线程是不能调用加了锁的类方法的,但是可以调用没有加同步关键字的方法或者加了同步关键字的实例方法,也就是说每个类有且仅有11
个锁,每个实例有且仅有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
34public void synSay() {
String x = "";
System.out.println("come in synSay----" + Thread.currentThread().getName());
synchronized (x) {
System.out.println("come in synchronized----" + Thread.currentThread().getName());
while (true) { //保证进入该方法的线程 一直占用着该同步方法
}
}
}
public static void main(String[] args){
Test test1 = new Test();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
test1.synSay();
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000); //休眠3秒钟 保证线程t1先执行
} catch (InterruptedException e) {
e.printStackTrace();
}
test1.synSay();
}
});
t1.start();
t2.start();
}
运行输出1
2
3come in synSay----Thread-0
come in synchronized----Thread-0
come in synSay----Thread-1
可以发现同步代码块和同步实例方法、同步类方法其实差不多,但是同步代码块将同步的范围缩小了,可以同步到指定的对象上,而不像同步实例方法、同步类方法那样同步的是整个方法,所以同步代码块在效率上比其他两者都有较大的提升。
需要注意的是,当同步代码块的时候,在类方法中加入同步代码块且同步的对象是xx.class
等类的引用的时候,同步的是该类,如果在**实例方法中加入同步代码块且同步的对象是this
,那么同步的是该实例,可以看成前者使用的是类的锁,后者使用的是实例的锁。
synchronized的特性
建议把volatile
的特性和synchronized
的特性进行对比学习,加深理解。《Java volatile关键字解析》
synchronized与可见性
JMM
关于synchronized
的两条语义规定了:
- 线程加锁前:需要将工作内存清空,从而保证了工作区的变量副本都是从主存中获取的最新值。
- 线程解锁前;需要将工作内存的变量副本写回到主存中。
大概流程:清空线程的工作内存->在主存中拷贝变量副本到工作内存->执行完毕->将变量副本写回到主存中->释放锁。
所以synchronized
能保证共享变量的可见性,而实现这个流程的原理也是通过插入内存屏障,和关键字volatile
相似。
synchronized与有序性
因为synchronized
是给共享变量加锁,即使用阻塞的同步机制,共享变量只能同时被一个线程操作,所以JMM
不用像volatile
那样考虑加内存屏障去保证synchronized
多线程情况下的有序性,因为CPU
在单线程情况下是保证了有序性的。
所以synchronized
修饰的代码,是保证了有序性的。
synchronized与原子性
同样因为synchronized
是给共享变量加锁了,以阻塞的机制去同步,在对共享变量进行读/写操作的时候是原子性的。
所以synchronized
修饰的代码,是能保证原子性的。
参考
Java并发编程的艺术
内存可见性和原子性:Synchronized和Volatile的比较
java synchronized类锁,对象锁详解(转载)