java多线程-初探(二)
本章主要阐述synchronized同步关键字,以及未做同步将出现的问题。
未做同步引发的问题举例
本文举例:1万个人一起吃1万碗饭,两个人随便吃。吃完为止。
正常想要的结果是:吃到第0碗的时候,程序执行结束,打印出剩余的饭为0碗。
未加同步可能出现的结果是:吃到第0碗的时候,有一个人以为还有饭可以吃,接着吃。结果就把剩余的饭吃出负数来了。
未加同步的问题不是每次执行都能出现的,这个要看cpu的资源执行效果,如果都是很和谐的状态下则不会出现,建议多执行几次。
class Main
package com.thread.three;
public class Main {
// 一万碗饭
public static int riceNum = 10000 ;
/**
* 吃饭
* @param args
*/
public static void main(String[] args) {
System.out.println("开吃!");
// 一万个线程同时吃饭
int peopleNum = 10000 ;
Thread[] threads = new Thread[peopleNum];
for (int i=0; i<peopleNum; i++){
Thread thread = new Thread(){
public void run(){
while (true){
if (Main.riceNum <= 0) break;
System.out.println("吃了一碗,剩余:" + Main.riceNum--);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
thread.start();
threads[i] = thread;
}
// 加入主线程
for (Thread thread : threads){
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("吃完啦,剩余:" + riceNum + "碗饭");
}
}
执行结果
问题原因
上述试了几遍出现的同步问题,说到底就是有一个线程以为还有饭可以吃,吃的时候却已经没了。
关键性问题代码:
if (Main.riceNum <= 0) break;
System.out.println("吃了一碗,剩余:" + Main.riceNum--);
举例说明
此时饭剩下最后1碗的时候,线程1在第一行判断通过,还未执行第二行减少饭的代码逻辑时,cpu切换到线程2,此时线程2以为饭还有1碗,线程2的判断也通过,当线程2要执行第二行时cpu又切换到线程3。
以此类推,假设有3个线程在第二行等待,线程1执行了第二行代码,现在饭变成0碗了,输出剩余0碗饭。cpu又切换到线程2,此时输出剩余-1碗饭,cpu再次切换到线程3,则输出剩余-2碗饭。
当cpu切换到某一个线程时,从上次切换走的那行继续执行,所以切换到线程2跟线程3时不需要判断饭是不是小于0碗了,因为cpu切走之前已经判断通过了。
解决思路
在线程1执行判断以及减少饭的操作时,其他线程等线程1吃完再吃呢?
我们要把判断是否还有饭以及吃饭的整个过程,不允许其他线程在我们吃的时候也来一起吃。
synchronized关键字
synchronized之内的代码块可以只让一个线程执行,执行完了才能轮到其他线程进入这个代码块,否则其他线程就在synchronized处等待。
synchronized可修饰方法或者方法内的代码块。
修饰方法的锁则是调用该方法的对象,如果同时new多个实例,则不是同一把锁。
synchronized的锁
实现其他线程等待,归根需要一把锁。这个锁建议要是一个唯一不可变的实例对象。
当线程1执行到synchronized时,获取了synchronized的锁,当线程1在执行代码块的时,cpu切换到其他线程,其他线程要获取锁的时候,发现锁已经被线程1占用了,无法获取,其他线程则在synchronized代码块之外等待线程1把锁释放了,才能获取锁从而执行。
大白话解释就是:当一个人拿了一把钥匙开门进房间了,其他人要进来时没有钥匙,只能等里面的人出来把钥匙给其他人,其他人才能进去。
Java中的同步有三种:
- 同步代码块。锁是由开发者自己定义
- 非静态方法上加同步。锁对象是当前调用方法的那个对象使用this 表示
- 静态方法上加同步。锁对象是当前方法所在的class文件,使用类名.class 表示
同步方法的执行:
当任何的线程要进入到当前这个方法时,就必须先判断能不能获取到锁,如果可以获取到,才能进入方法运行,获取不到,只能在方法外面等待。
synchronized代码格式
synchronized( 任意对象(锁) ){
需要被同步的代码
}
释放锁
上述为例,线程1释放锁有两种方式。
1:synchronized代码块内执行完成。
2:synchronized代码块内发生异常且代码块内无捕获。
添加synchronized锁的关键性代码示例
while (true){
synchronized (Main.class){
if (Main.riceNum <= 0) break;
System.out.println("吃了一碗,剩余:" + Main.riceNum--);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
上述的锁就是:Main.class
这个class对象在类加载时产生,唯一且不可变。
错误示例
while (true){
synchronized (new Object()){
if (Main.riceNum <= 0) break;
System.out.println("吃了一碗,剩余:" + Main.riceNum--);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
上文的锁是一个Object对象,造成的结果就是锁直接在门上插着,谁都能进。
没new一个对象都不一样,两个线程的锁不是同一个,则无法实现同步。
线程安全的类
线程安全:效率低。
线程不安全:效率高。
在一个类里,如果所有的方法都被synchronized修饰,则称这个类为线程安全的类。
能保证同一时间内,有且只有一个线程可以进入这个类里修改操作数据。从而保证数据的安全性,避免因为同步而导致出现的脏数据(如上文的剩余-1碗饭)
举例说明:StringBuffer和StringBuilder
StringBuffer:线程安全的类
StringBuilder:线程不安全的类
StringBuffer的方法中,所有方法都被synchronized修饰。因为无法多个线程一起操作,所以效率相对StringBuffer比较低。
举例说明:HashMap和Hashtable
Hashtable:线程安全的类,效率低
HashMap:线程不安全的类,效率高
举例说明:ArrayList和Vector
Vector:线程安全的类,效率低
ArrayList:线程不安全的类,效率高
说明
线程安全的类,表示不可以多个线程同时访问一个线程安全的对象。
如上文的集合,不安全的类,则可以两个线程同时添加数据。安全的类则必须等第一个线程添加完成后第二个线程才能添加数据。(前提是两个线程操作同一个集合对象)
注:线程不安全的集合也可以通过Collections工具类转换成线程安全的。
如:List<Integer> list = Collections.synchronizedList(new ArrayList());