在单线程程序中,每次只做一件事情,后面的事情等待前面的事情完成才进行,但如果使用多线程程序,就会出现多个线程抢占资源的问题,线程的优先级部分地解决了这个问题,但还不够,Java提供线程同步机制来防止资源访问的冲突。
先举一个例子来阐述线程不同步导致的问题:这类似于一个售票系统,查询票剩余数量,如果有票就买一张,剩余票量减1,初始有10张票。
public class ThreadSafeTest implements Runnable{
int num=10;
public void run(){
while(true){
if(num>0){
try{
Thread.sleep(100);
}catch (Exception e){
e.printStackTrace();
}
System.out.println("tickets"+num--);
}
}
}
public static void main(String[] args){
ThreadSafeTest t=new ThreadSafeTest();//实例化类对象
Thread t1=new Thread(t);//以类对象分别实例化4个线程
Thread t2=new Thread(t);
Thread t3=new Thread(t);
Thread t4=new Thread(t);
t1.start();//分别启动线程
t2.start();
t3.start();
t4.start();
}
}
输出结果:
tickets10
tickets8
tickets9
tickets7
tickets6
tickets3
tickets4
tickets5
tickets2
tickets1
tickets0
tickets-1
tickets-2
剩余票量竟然出现了负数,说明程序出现问题,这是因为多个线程同时访问数据,前一个线程将票售出时,后一个线程已经完成了是否有票的判断。
为了解决这个资源共享冲突问题,一种解决方式是同一时间只允许一个线程访问资源,在Java同步机制中,使用synchronized关键字。
synchronized关键字用来修饰需要设置访问限制的程序块或方法,以同步块为例解释上面的买票问题:
同步块的语法是:
synchronized(Object){
//同步块
}
上面的例子,经过synchronized修饰,就能运行正确(这里Object是null)
public class ThreadSafeTest implements Runnable{
int num=10;
public void run(){
while(true){
synchronized(""){
if(num>0){
try{
Thread.sleep(100);
}catch (Exception e){
e.printStackTrace();
}
System.out.println("tickets"+--num);
}
}
}
}
public static void main(String[] args){
ThreadSafeTest t=new ThreadSafeTest();
Thread t1=new Thread(t);
Thread t2=new Thread(t);
Thread t3=new Thread(t);
Thread t4=new Thread(t);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
结果
tickets9
tickets8
tickets7
tickets6
tickets5
tickets4
tickets3
tickets2
tickets1
tickets0
但程序明显没有先前快了,同时还想到在真正的售票系统中,肯定不能等一个人买票付款确认完了才能下一个人买,如何解决访问和资源之间的冲突,应该就是所谓高并发的命题了!
当然synchronized还可以修饰方法,系统性的介绍如下:
synchronized是Java的一个关键字,它包括两种用法:synchronized 方法和 synchronized 块,用以解决同步问题。
有四种不同的同步方式:
1,实例方法同步
2,静态方法同步
3,实例方法中同步块
4,静态方法中同步块
下面是一个同步的实例方法:
public synchronized void add(int value){
this.count += value;
}
synchronized告诉Java该方法是同步的。
Java实例方法同步是同步在拥有该方法的对象上。这样,每个实例其方法同步都同步在不同的对象上,即该方法所属的实例。这样的一个实例方法同时只能被一个线程调用,但多个实例可以被多个线程调用,总结来说就是“一实例一线程”。
静态方法同步和实例方法同步方法一样,也使用synchronized 关键字。Java静态方法同步如下示例:
public static synchronized void add(int value){
count += value;
}
同样,这里synchronized 关键字告诉Java这个方法是同步的。
静态方法的同步是指同步在该方法所在的类上。
因为在Java虚拟机中一个静态方法是唯一地存放在方法区中,由实例直接引用,所以同时只允许一个线程执行静态同步方法。
对于不同类中的静态同步方法,一个线程可以执行每个类中的静态同步方法而无需等待。
同一个类的不同静态同步方法,不能被多线程同时执行,因为静态方法能操作静态变量,静态变量对于一个类而言也是唯一的,同时调用不同静态方法,如果操作了相同的静态变量也会出现同步问题。
理解静态与非静态的区别,需要先理解Java内存管理的基础以及static关键字的知识。在我学习了这两块知识之后,就很轻松理解了。关于这两个,有笔记在Java内存分析(1)——基本内存管理机制和Java知识碎片整理(5)——static关键字
非静态同步:
一个实例的非静态同步方法只能被一个线程使用;
一个实例的非静态同步方法被使用时,它的其他非同步方法可以被其他线程使用
静态同步:
一个类的任何(相同或不同)静态同步方法,不能被多线程同时执行
一个类的静态同步方法被使用时,它的其他非静态同步的方法可以被其它线程使用
生硬的逻辑有些晦涩,以下比喻更容易理解:
打个比方:一个object就像一个大房子,大门永远打开。房子里有 很多房间(也就是方法)。这些房间有上锁的(synchronized方法), 和不上锁之分(普通方法)。房门口放着一把钥匙(key),这把钥匙可以打开所有上锁的房间。另外我把所有想调用该对象方法的线程比喻成想进入这房子某个房间的人。所有的东西就这么多了,下面我们看看这些东西之间如何作用的。在此我们先来明确一下我们的前提条件。该对象至少有一个synchronized方法,否则这个key还有啥意义。当然也就不会有我们的这个主题了。
一个人想进入某间上了锁的房间,他来到房子门口,看见钥匙在那儿(说明暂时还没有其他人要使用上锁的 房间)。于是他走上去拿到了钥匙,并且按照自己的计划使用那些房间。注意一点,他每次使用完一次上锁的房间后会马上把钥匙还回去。即使他要连续使用两间上锁的房间,中间他也要把钥匙还回去,再取回来。因此,普通情况下钥匙的使用原则是:“随用随借,用完即还。”这时其他人可以不受限制的使用那些不上锁的房间,一个人用一间可以,两个人用一间也可以,没限制。但是如果当某个人想要进入上锁的房间,他就要跑到大门口去看看了。有钥匙当然拿了就走,没有的话,就只能等了。要是很多人在等这把钥匙,等钥匙还回来以后,谁会优先得到钥匙?Not guaranteed。象前面例子里那个想连续使用两个上锁房间的家伙,他中间还钥匙的时候如果还有其他人在等钥匙,那么没有任何保证这家伙能再次拿到。 (JAVA规范在很多地方都明确说明不保证,象 Thread.sleep()休息后多久会返回运行,相同优先权的线程那个首先被执行,当要访问对象的锁被 释放后处于等待池的多个线程哪个会优先得到,等等。我想最终的决定权是在JVM,之所以不保证,就是因为JVM在做出上述决定的时候,绝不是简简单单根据 一个条件来做出判断,而是根据很多条。而由于判断条件太多,如果说出来可能会影响JAVA的推广,也可能是因为知识产权保护的原因吧。SUN给了个不保证就混过去了,无可厚非。但我相信这些不确定,并非完全不确定。因为计算机这东西本身就是按指令运行的。即使看起来很随机的现象,其实都是有规律可寻。学过计算机的都知道,计算机里随机数的学名是伪随机数,是人运用一定的方法写出来的,看上去随机罢了。另外,或许是因为要想弄的确定太费事,也没多大意义,所 以不确定就不确定了吧。)
若将一个大的方法声明为synchronized 将会大大影响效率,典型地,若将线程类的方法 run() 声明为 synchronized ,由于在线程的整个生命期内它一直在运行,因此将导致它对本类任何 synchronized 方法的调用都永远不会成功。当然我们可以通过将访问类成员变量的代码放到专门的方法中,将其声明为 synchronized ,并在主方法中调用来解决这一问题,但是 Java 为我们提供了更好的解决办法,那就是 synchronized 块。
有时你不需要同步整个方法,而是同步方法中的一部分。Java可以对方法的一部分进行同步。
在非同步的Java方法中的同步块的例子如下所示:
public void add(int value){
synchronized(this){
this.count += value;
}
}
示例使用Java同步块构造器来标记一块代码是同步的。该代码在执行时和同步方法一样。
注意Java同步块构造器用括号将对象括起来。在上例中,使用了“this”,即为调用add方法的实例本身。在同步构造器中用括号括起来的对象叫做监视器对象。上述代码使用监视器对象同步,同步实例方法使用调用方法本身的实例作为监视器对象。
一次只有一个线程能够在同步于同一个监视器对象的Java方法内执行。
下面两个例子都同步他们所调用的实例对象上,因此他们在同步的执行效果上是等效的。
public class MyClass {
public synchronized void log1(String msg1, String msg2){
log.writeln(msg1);
log.writeln(msg2);
}
public void log2(String msg1, String msg2){
synchronized(this){
log.writeln(msg1);
log.writeln(msg2);
}
}
}
在上例中,每次只有一个线程能够在两个同步块中任意一个方法内执行。
如果第二个同步块不是同步在this实例对象上,那么两个方法可以被线程同时执行。
和上面类似,下面是两个静态方法同步的例子。这些方法同步在该方法所属的类对象上。
public class MyClass {
public static synchronized void log1(String msg1, String msg2){
log.writeln(msg1);
log.writeln(msg2);
}
public static void log2(String msg1, String msg2){
synchronized(MyClass.class){
log.writeln(msg1);
log.writeln(msg2);
}
}
}
这两个方法不允许同时被线程访问。
如果第二个同步块不是同步在MyClass.class这个对象上。那么这两个方法可以同时被线程访问。
Java同步实例
在下面例子中,启动了两个线程,都调用Counter类同一个实例的add方法。因为同步在该方法所属的实例上,所以同时只能有一个线程访问该方法。
public class Counter{
long count = 0;
public synchronized void add(long value){
this.count += value;
}
}
public class CounterThread extends Thread{
protected Counter counter = null;
public CounterThread(Counter counter){
this.counter = counter;
}
public void run() {
for(int i=0; i<10; i++){
counter.add(i);
}
}
}
public class Example {
public static void main(String[] args){
Counter counter = new Counter();
Thread threadA = new CounterThread(counter);
Thread threadB = new CounterThread(counter);
threadA.start();
threadB.start();
}
}
创建了两个线程。他们的构造器引用同一个Counter实例。Counter.add方法是同步在实例上,是因为add方法是实例方法并且被标记上synchronized关键字。因此每次只允许一个线程调用该方法。另外一个线程必须要等到第一个线程退出add()方法时,才能继续执行方法。
如果两个线程引用了两个不同的Counter实例,那么他们可以同时调用add()方法。这些方法调用了不同的对象,因此这些方法也就同步在不同的对象上。这些方法调用将不会被阻塞。如下面这个例子所示:
public class Example {
public static void main(String[] args){
Counter counterA = new Counter();
Counter counterB = new Counter();
Thread threadA = new CounterThread(counterA);
Thread threadB = new CounterThread(counterB);
threadA.start();
threadB.start();
}
}
注意这两个线程,threadA和threadB,不再引用同一个counter实例。CounterA和counterB的add方法同步在他们所属的对象上。调用counterA的add方法将不会阻塞调用counterB的add方法。
一、当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
二、然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。
三、尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。
四、以上规则对其它对象锁同样适用.