Java线程:线程的同步与锁

1、同步问题提出安全

线程的同步是为了防止多个线程访问一个数据对象时,对数据形成的破坏。
例如:两个线程ThreadA、ThreadB都操做同一个对象Foo对象,并修改Foo对象上的数据。
public class Foo { 
    private int x = 100; 

    public int getX() { 
        return x; 
    } 

    public int fix(int y) { 
        x = x - y; 
        return x; 
    } 
}
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值= 40 
Thread-B : 当前foo对象的x值= 40 
Thread-B : 当前foo对象的x值= -20 
Thread-A : 当前foo对象的x值= -50 
Thread-A : 当前foo对象的x值= -80 
Thread-B : 当前foo对象的x值= -80 

Process finished with exit code 0
从结果发现,这样的输出值明显是不合理的。缘由是两个线程不加控制的访问Foo对象并修改其数据所致。
 
若是要保持结果的合理性,只须要达到一个目的,就是将对Foo的访问加以限制,每次只能有一个线程在访问。这样就能保证Foo对象中数据的合理性了。
 
在具体的Java代码中须要完成一下两个操做:
把竞争访问的资源类Foo变量x标识为private;
同步哪些修改变量的代码,使用synchronized关键字同步方法或代码。
 
2、同步和锁定
 

一、锁的原理
 
Java中每一个对象都有一个内置锁
 
当程序运行到非静态的synchronized同步方法上时,自动得到与正在执行代码类的当前实例(this实例)有关的锁。得到一个对象的锁也称为获取锁、锁定对象、在对象上锁定或在对象上同步。
 
当程序运行到synchronized同步方法或代码块时才该对象锁才起做用。
 
一个对象只有一个锁。因此,若是一个线程得到该锁,就没有其余线程能够得到锁,直到第一个线程释放(或返回)锁。这也意味着任何其余线程都不能进入该对象上的synchronized方法或代码块,直到该锁被释放。
 
释放锁是指持锁线程退出了synchronized同步方法或代码块。
 
关于锁和同步,有一下几个要点:
1)、只能同步方法,而不能同步变量和类;
2)、每一个对象只有一个锁;当提到同步时,应该清楚在什么上同步?也就是说,在哪一个对象上同步?
3)、没必要同步类中全部的方法,类能够同时拥有同步和非同步方法。
4)、若是两个线程要执行一个类中的synchronized方法,而且两个线程使用相同的实例来调用方法,那么一次只能有一个线程可以执行方法,另外一个须要等待,直到锁被释放。也就是说:若是一个线程在对象上得到一个锁,就没有任何其余线程能够进入(该对象的)类中的任何一个同步方法。
5)、若是线程拥有同步和非同步方法,则非同步方法能够被多个线程自由访问而不受锁的限制。
6)、线程睡眠时,它所持的任何锁都不会释放。
7)、线程能够得到多个锁。好比,在一个对象的同步方法里面调用另一个对象的同步方法,则获取了两个对象的同步锁。
8)、同步损害并发性,应该尽量缩小同步范围。同步不但能够同步整个方法,还能够同步方法中一部分代码块。
9)、在使用同步代码块时候,应该指定在哪一个对象上同步,也就是说要获取哪一个对象的锁。例如:
    public int fix(int y) {
        synchronized (this) {
            x = x - y;
        }
        return x;
    }
 
固然,同步方法也能够改写为非同步方法,但功能彻底同样的,例如:
    public synchronized int getX() {
        return x++;
    }
    public int getX() {
        synchronized (this) {
            return x;
        }
    }
效果是彻底同样的。
 
3、静态方法同步
 
要同步静态方法,须要一个用于整个类对象的锁,这个对象是就是这个类(XXX.class)。
例如:
public static synchronized int setName(String name){
      Xxx.name = name;
}
等价于
public static int setName(String name){
      synchronized(Xxx.class){
            Xxx.name = name;
      }
}
 
4、若是线程不能不能得到锁会怎么样
 
若是线程试图进入同步方法,而其锁已经被占用,则线程在该对象上被阻塞。实质上,线程进入该对象的的一种池中,必须在哪里等待,直到其锁被释放,该线程再次变为可运行或运行为止。
 
当考虑阻塞时,必定要注意哪一个对象正被用于锁定:
一、调用同一个对象中非静态同步方法的线程将彼此阻塞。若是是不一样对象,则每一个线程有本身的对象的锁,线程间彼此互不干预。
 
二、调用同一个类中的静态同步方法的线程将彼此阻塞,它们都是锁定在相同的Class对象上。
 
三、静态同步方法和非静态同步方法将永远不会彼此阻塞,由于静态方法锁定在Class对象上,非静态方法锁定在该类的对象上。
 
四、对于同步代码块,要看清楚什么对象已经用于锁定(synchronized后面括号的内容)。在同一个对象上进行同步的线程将彼此阻塞,在不一样对象上锁定的线程将永远不会彼此阻塞。
 
5、什么时候须要同步
 
在多个线程同时访问互斥(可交换)数据时,应该同步以保护数据,确保两个线程不会同时修改更改它。
 
对于非静态字段中可更改的数据,一般使用非静态方法访问。
对于静态字段中可更改的数据,一般使用静态方法访问。
 
若是须要在非静态方法中使用静态字段,或者在静态字段中调用非静态方法,问题将变得很是复杂。已经超出
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 (String) nameList.remove(0); 
        } else { 
            return null; 
        } 
    } 
}
public class Test { 
    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(); 
    } 
}

 

范围了。
 
6、线程安全类
 
当一个类已经很好的同步以保护它的数据时,这个类就称为“线程安全的”。
 
即便是线程安全类,也应该特别当心,由于操做的线程是间仍然不必定安全。
 
举个形象的例子,好比一个集合是线程安全的,有两个线程在操做同一个集合对象,当第一个线程查询集合非空后,删除集合中全部元素的时候。第二个线程也来执行与第一个线程相同的操做,也许在第一个线程查询后,第二个线程也查询出集合非空,可是当第一个执行清除后,第二个再执行删除显然是不对的,由于此时集合已经为空了。
看个代码:
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 (String) nameList.remove(0); 
        } else { 
            return null; 
        } 
    } 
}
public class Test { 
    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(); 
    } 
}
虽然集合对象
    private List nameList = Collections.synchronizedList(new LinkedList());
是同步的,可是程序还不是线程安全的。
出现这种事件的缘由是,上例中一个线程操做列表过程当中没法阻止另一个线程对列表的其余操做。
 
解决上面问题的办法是,在操做集合对象的NameList上面作一个同步。改写后的代码以下:
 
public class NameList { 
    private List nameList = Collections.synchronizedList(new LinkedList()); 

    public synchronized void add(String name) { 
        nameList.add(name); 
    } 

    public synchronized String removeFirst() { 
        if (nameList.size() > 0) { 
            return (String) nameList.remove(0); 
        } else { 
            return null; 
        } 
    } 
}
这样,当一个线程访问其中一个同步方法时,其余线程只有等待。
 
7、线程死锁
 
死锁对Java程序来讲,是很复杂的,也很难发现问题。当两个线程被阻塞,每一个线程在等待另外一个线程时就发生死锁。
 
仍是看一个比较直观的死锁例子:
public class DeadlockRisk { 
    private static class Resource { 
        public int value; 
    } 

    private Resource resourceA = new Resource(); 
    private Resource resourceB = new Resource(); 

    public int read() { 
        synchronized (resourceA) { 
            synchronized (resourceB) { 
                return resourceB.value + resourceA.value; 
            } 
        } 
    } 

    public void write(int a, int b) { 
        synchronized (resourceB) { 
            synchronized (resourceA) { 
                resourceA.value = a; 
                resourceB.value = b; 
            } 
        } 
    } 
}
假设read()方法由一个线程启动,write()方法由另一个线程启动。读线程将拥有resourceA锁,写线程将拥有resourceB锁,二者都坚持等待的话就出现死锁。
 
实际上,上面这个例子发生死锁的几率很小。由于在代码内的某个点,CPU必须从读线程切换到写线程,因此,死锁基本上不能发生。
 
可是,不管代码中发生死锁的几率有多小,一旦发生死锁,程序就死掉。有一些设计方法能帮助避免死锁,包括始终按照预约义的顺序获取锁这一策略。已经超出SCJP的考试范围。
 
8、线程同步小结
 
一、线程同步的目的是为了保护多个线程反问一个资源时对资源的破坏。
二、线程同步方法是经过锁来实现,每一个对象都有切仅有一个锁,这个锁与一个特定的对象关联,线程一旦获取了对象锁,其余访问该对象的线程就没法再访问该对象的其余同步方法。
三、对于静态同步方法,锁是针对这个类的,锁对象是该类的Class对象。静态和非静态方法的锁互不干预。一个线程得到锁,当在一个同步方法中访问另外对象上的同步方法时,会获取这两个对象锁。
四、对于同步,要时刻清醒在哪一个对象上同步,这是关键。
五、编写线程安全的类,须要时刻注意对多个线程竞争访问资源的逻辑和安全作出正确的判断,对“原子”操做作出分析,并保证原子操做期间别的线程没法访问竞争资源。
六、当多个线程等待一个对象锁时,没有获取到锁的线程将发生阻塞。
七、死锁是线程间相互等待锁锁形成的,在实际中发生的几率很是的小。真让你写个死锁程序,不必定好使,呵呵。可是,一旦程序发生死锁,程序将死掉。
相关文章
相关标签/搜索