线程同步

来源:互联网 发布:淘宝定制的好不好 编辑:程序博客网 时间:2024/06/01 17:44
</pre><p></p><pre name="code" class="java"><a target=_blank target="_blank" href="http://lavasoft.blog.51cto.com/62575/99155"></a>
点击打开链接

一、同步问题提出

线程的同步是为了防止多个线程访问一个数据对象时,对数据造成的破坏。
例如:两个线程ThreadA、ThreadB都操作同一个对象Foo对象,并修改Foo对象上的数据。

package com.test.thloc.dog;public class Foo {private int x = 100;     public int getX() {         return x;     }     public int fix(int y) {         x = x - y;         return x;     }}

package com.test.thloc.dog;public class MyRunnable implements Runnable{private Foo foo = new Foo();     public static void main(String[] args) {         MyRunnable r = new MyRunnable();         Thread ta = new Thread(r, "Thread-A");         Thread tb = new Thread(r, "Thread-B");         ta.start();         tb.start();     }     public void run() {         for (int i = 0; i < 3; i++) {             this.fix(30);             try {                 Thread.sleep(1);             } catch (InterruptedException e) {                 e.printStackTrace();             }             System.out.println(Thread.currentThread().getName() + " : 当前foo对象的x值= " + foo.getX());         }     }     public int fix(int y) {         return foo.fix(y);     } }

运行结果:



Thread-A : 当前foo对象的x值= 40Thread-B : 当前foo对象的x值= 40Thread-B : 当前foo对象的x值= -20Thread-A : 当前foo对象的x值= -20Thread-A : 当前foo对象的x值= -80Thread-B : 当前foo对象的x值= -80

这样的结果明显是不合理的,因为我们想得到的结果是一个线程处理的时候另一个线程不要处理。原因是两个线程不加控制的访问Foo对象并修改其数据所致。

如果要保持结果的合理性,只需要达到一个目的,就是将对Foo的访问加以限制,每次只能有一个线程在访问。这样就能保证Foo对象中数据的合理性了。

在具体的Java代码中需要完成以下两个操作:
把竞争访问的资源类Foo变量x标识为private;
同步哪些修改变量的代码,使用synchronized关键字同步方法或代码。

二、同步和锁定


1、锁的原理

1. 类锁:在代码中的方法上加了static和synchronized的锁,或者synchronized(xxx.class)的代码段 


2.对象锁:在代码中的方法上加了synchronized的锁,或者synchronized(this)的代码段 


3.私有锁:在类内部声明一个私有属性如private Object lock,在需要加锁的代码段synchronized(lock), 


java中每一个对象都有一个内置锁。当程序运行到非静态的synchronized同步方法上时。自动获得与正在执行代码类的当前实例(this实例)有关的锁。
获得对象的锁,也称为获取锁,锁定对象,在对象上锁定或在对象上同步。

当程序运行到synchronized同步方法或者同步代码块时该对象锁才会起作用。

一个对象只有一个锁,所以只要有一个线程获得这个锁,那么其他的线程就不能获得这个锁。直到获得锁的线程释放锁。

释放锁是指 :持锁的线程退出了synchronized方法或者代码块。

关于锁和同步,有以下几点:

 1)、只能同步方法,不能同步变量和类。
 2)、每个对象只有一个锁,当提到同步时,应该清楚在什么上同步,也就是说,在那个对象上同步。
 3)、不必同步类中的所以的方法,类中可以有同步的方法也可以有非同步的方法。
 4)、如果两个线程要执行一个类的synchronized方法。并且两个线程操作的是同一个实例,那么一次只能有一个线程执行这个方法,另一个线程需要等待,知道锁被释放。
也就是说如果一个线程获得了一个对象的锁,那么就没有任何其他的线程可以进入这个对象类中的任何其他同步方法。
 5)、
 5)、如果线程拥有同步和非同步的方法,那么非同步方法可以被多个线程自由访问而不受限制。
 6)、线程睡眠的时候,线程持有的锁,并不会释放。
 7)、线程可以获得多个锁、比如,在一个对象的同步方法里,需要调用另外一个对象的同步方法,则获取了两个对象的同步锁。
 8)、同步损害并发,应该尽可能的缩小同步的范围,同步不但可以同步整个方法、还可以同步方法中的一部分代码。
 9)、 使用同步代码块的时候,应该指定,在那个对象上同步,也就是说要获得那个对象的锁例如:

 public int fix(int y) {        synchronized (this) {            x = x - y;        }        return x;    }

三、静态方法同步


要同步静态方法,需要一个作用于整个类对象的锁,这个类对象就是这个类(xxx.class) 
例如:


public static synchronized int setName(String name){      Xxx.name = name;}

等价于

public static int setName(String name){      synchronized(Xxx.class){            Xxx.name = name;      }}

四、如果线程不能获得锁会怎么样

如果线程试图进入同步方法,而其锁已经被占用,则线程在该对象上是阻塞的,实质上,线程进入对象的一种池中,必须在那里等待,直到其锁被释放

当考虑阻塞时,一定要注意哪个对象是被用于锁定的:
1、调用同一个对象中非静态的同步方法的时候,线程彼此阻塞。如果是不同的对象,则每个线程拥有自己的对象锁。线程间彼此之间不干预。

2、调用同一个类中的静态同步方法的线程将彼此阻塞,他们都是锁定在相同的Class对象上的。

3、静态同步方法和非静态同步方法将永远不会彼此阻塞,因为静态同步方法锁定在Class对象上,非静态的同步方法锁定在该类的对象上。

4、对于同步代码块,要看清楚什么对象已经用于锁定(synchronized括号后面的内容)。在同一个对象上进行同步的线程将彼此阻塞,在不同对象上锁定的线程将永远不会彼此阻塞。

五、何时需要同步

在多个线程同时访问互斥数据时。应该同步以保护数据。确保两个线程不会同时修改它。


对于非静态字段中可更改的数据,通常使用非静态方法访问。
对于静态字段中可更改的数据,通常使用静态方法访问。

如果需要在非静态方法中使用静态字段,或者在静态字段中调用非静态方法,问题将变得非常复杂。已经超出SJCP考试范围了。


六、线程安全类

当一个类已经很好的同步以保护它的数据时,这个类称为“线程安全的”。

即使是线程安全的类,使用的时候也应该小心,因为操作他的线程不一定安全。

举个形象的例子,比如一个集合是线程安全的,有两个线程在操作同一个集合对象,当第一个线程查询集合非空后,删除集合中所有元素的时候。第二个线程也来执行与第一个线程相同的操作,也许在第一个线程查询后,第二个线程也查询出集合非空,但是当第一个执行清除后,第二个再执行删除显然是不对的,因为此时集合已经为空了。
看个代码:


package com.test.thloc.dog;import java.util.Collections;import java.util.LinkedList;import java.util.List;public class NameList {private List nameList = Collections.synchronizedList(new LinkedList());public void add(String name) {nameList.add(name);}public String removeFirst() {if (nameList.size() > 0) {return Thread.currentThread().getName() + " : " + (String) nameList.remove(0);} else {return Thread.currentThread().getName() + " : " + "list is null";}}}


package com.test.thloc.dog;public class NameMain {  public static void main(String[] args) {         final NameList nl = new NameList();         nl.add("aaa");         class NameDropper extends Thread{             public void run(){                 String name = nl.removeFirst();                 System.out.println(name);             }         }         Thread t1 = new NameDropper();         Thread t2 = new NameDropper();         t1.start();         t2.start();     } }

同步的时候正确输出效果:

Thread-0 : aaaThread-1 : list is null

非同步的时候错误情况:

Exception in thread "Thread-1" Thread-0 : aaajava.lang.IndexOutOfBoundsException: Index: 0, Size: 0at java.util.LinkedList.checkElementIndex(Unknown Source)at java.util.LinkedList.remove(Unknown Source)at java.util.Collections$SynchronizedList.remove(Unknown Source)at com.test.thloc.dog.NameList.removeFirst(NameList.java:18)at com.test.thloc.dog.NameMain$1NameDropper.run(NameMain.java:11)

出现错误的原因就是第一个线程已经把list清空了,但是第二个线程进入的时候list不是空的,当第一个线程清空后,第二个线程在指向删除操纵的时候就会出现错误了。

虽然集合对象
    private List nameList = Collections.synchronizedList(new LinkedList());
是同步的,但是程序还不是线程安全的。
出现这种事件的原因是,上例中一个线程操作列表过程中无法阻止另外一个线程对列表的其他操作。

解决上面问题的办法是,在操作集合对象的NameList上面做一个同步。改写后的代码如下:

package com.test.thloc.dog;import java.util.Collections;import java.util.LinkedList;import java.util.List;public class NameList {private List nameList = Collections.synchronizedList(new LinkedList());public void add(String name) {nameList.add(name);}public  synchronized  String removeFirst() {if (nameList.size() > 0) {return Thread.currentThread().getName() + " : " + (String) nameList.remove(0);} else {return Thread.currentThread().getName() + " : " + "list is null";}}}


这样当一个线程访问清空方法的时候,另一个线程只能等待了,

线程同步小结:

1、线程同步是为了当多个线程访问同一个资源的时候,对资源的保护。

2、线程同步方法是通过锁类实现的,每一个对象都有且只有一个锁,这个锁与一个特定的对象关联。线程一旦获取了对象锁,其他访问该对象的线程就无法访问该对象的其他同步方法


3、对于静态同步方法,锁是针对这个类的,锁对象是该类的Class对象。静态和非静态的方法的锁是互补干预的。线程在访问同步方法的中,如果需要访问其他对象的同步方法那么这个线程获得两个对象的锁。

4、对于同步要时刻清楚要是在那个对象上同步。

5、编写线程安全的类,需要时刻注意对多个线程竞争访问资源的逻辑和安全做出正确的判断,对“原子”操作做出分析。并保证原子操作期间别的线程无法访问资源。

0 0
原创粉丝点击