什么时候会出现多线程问题以及出现多线程问题的条件有哪些,下面通过一个简单的例子来看下。
假如系统中需要生成累加不重复的数字,用来设置单号或流水号,我们通过一段简单的代码来实现:
public class Thread4 {
private int num;
private int getNext() {
return num++;
}
public static void main(String[] args) throws InterruptedException {
Thread4 t4 = new Thread4();
while(true) {
Thread.sleep(500);
System.out.println(t4.getNext());
}
}
}
只用单线程来获取值,输出0开始依次递增数字,单线程下没有任何问题。
然后开启三个线程来操作:
public class Threadt3 {
private int num;
private int getNext() {
return num++;
}
public static void main(String[] args) {
Threadt3 t3 = new Threadt3();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + t3.getNext());
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + t3.getNext());
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + t3.getNext());
}
}
}).start();
}
}
看下结果:
可以看出,在多线程场景下出现了明显的问题,多个线程获取到的值是一样的,出现了值重复的现象。
出现多线程安全性问题必要条件:
那怎么解决上述问题呢,只需使用synchronized关键字,使方法变为同步方法:
private int num;
private synchronized int getNext() {
return num++;
}
这样,保证了方法只能同时被一个线程访问,其余线程处于阻塞等待状态。
在Java中,每一个对象都拥有一个对象锁,多线程同时访问某个对象时,线程只有获取了该对象的锁才能访问这个对象。在Java中,可以使用synchronized关键字来修饰一个方法或者代码块,synchronized不能用来修饰变量,当某个线程调用该对象的synchronized方法或者访问synchronized代码块时,这个线程便获得了该对象的锁,其他线程暂时无法访问这个方法,只有等待这个方法执行完毕或者代码块执行完毕,这个线程才会释放该对象的锁,其他线程才能执行这个方法或者代码块。
synchronized的使用一,synchronized修饰方法:
private synchronized int getNext() {
return num++;
}
private static synchronized int getNext() {
return num++;
}
synchronized修饰方法时放在返回类型void等之前,能修饰非静态的实例方法和静态方法,修饰实例方法时锁住的是实例对象,修改静态方式时锁住的是类字节码Class对象。
synchronized的使用二,synchronized修饰代码块,修饰方法体内某一代码块:
private Integer index;
//修饰Object对象
private int getNext() {
synchronized (index) {
return -1;
}
}
//修饰类的实例
private int getNext() {
synchronized (this) {
return -1;
}
}
//修饰类的class对象
private int getNext() {
synchronized (Threadt3.class) {
return -1;
}
}
synchronized修饰代码块时,synchronized后括号中可接类的实例对象this、class对象、任意对象Object,锁住的也是括号中的对象。
修饰分类 | 修饰对象 | 被锁资源 | 说明 |
修饰方法 | 实例方法(非静态) | 类的实例对象 | public synchronized int getNum(){ } |
静态方法 | 类的class对象(字节码) | public static synchronized int getName(){ } | |
修饰代码块 | 任意Object对象 | Object对象本身 | Integer n=1; synchronized(n){ } |
类的实例对象 | 类实例对象 | synchronized(this){ } | |
class对象 | 类class对象(字节码) | synchronized(Person.class){ } |
3、synchronized的jvm实现原理解析
我们用一个简单的同步方法,通过查看字节码方式来验证下。
同步方法:
public class Threadt5 {
private Integer val = 2;
public int getNum() {
synchronized (val) {
val ++;
return val;
}
}
public static void main(String[] args) {
Threadt5 t5 = new Threadt5();
System.out.println(t5.getNum());
}
}
上述在getNum()方法中有个synchronized同步块,锁住的对象是val对象,我们看下getNum()方法的字节码:
(javap -verbose查看字节码)
如果把方法getNum()改为非同步:
public int getNum() {
val ++;
return val;
}
再查看字节码:
对比上述两字节码可以发现,同步方法字节码比非同步的多出monitor对象的操作,分别是monitorenter、monitorexit,可见,jvm实现同步代码块的原理是基于进入和退出monitor对象来实现的,monitorenter、monitorexit是字节码指令,monitorenter代表进入同步代码块,monitorexit表示同步结束。
(在上一节中我们提到了java的每个对象,不管是Object还是类的class,都有对象锁,都具备锁信息,那么,对象的锁信息存在对象的什么地方呢?每个java对象都有对象头,锁的信息就存放在对象头中。)
那么monitor是什么呢?monitor是存在于每个java对象中,java中每个对象(Object/class)都有唯一的一个monitor,且同时只能有一个线程可以获取某个对象的monitor。任意线程对Object的访问,首先要获得Object的监视器,如果获取失败,该线程就进入同步状态,线程状态变为BLOCKED,当Object的监视器占有者释放后,在同步队列中得线程就会有机会重新获取该监视器。
monitor的概念:可以说monitor是一种机制也可以说是一个对象,操作系统为进一步优化并发问题,提出了monitor概念,操作系统本身并不支持 monitor 机制,实际上,monitor 是属于编程语言的范畴,当你想要使用 monitor 时,先了解一下语言本身是否支持 monitor 原语,例如 C 语言它就不支持 monitor,Java 语言支持 monitor,简单的说,是操作系统范畴的,在java字节码中使用monitorenter、monitorexit就能传达至操作系统。