java

位置:IT落伍者 >> java >> 浏览文章

Java线程:线程的同步与锁


发布日期:2019年08月25日
 
Java线程:线程的同步与锁

同步问题提出

线程的同步是为了防止多个线程访问一个数据对象时对数据造成的破坏

例如两个线程ThreadAThreadB都操作同一个对象Foo对象并修改Foo对象上的数据

public class Foo {

private int x = ;

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 ThreadA

Thread tb = new Thread(r ThreadB

tastart()

tbstart()

}

public void run() {

for (int i = ; i < ; i++) {

thisfix(

try {

Threadsleep(

} catch (InterruptedException e) {

eprintStackTrace()

}

Systemoutprintln(ThreadcurrentThread()getName() + : 当前foo对象的x值= + foogetX())

}

}

public int fix(int y) {

return foofix(y)

}

}

运行结果

ThreadA : 当前foo对象的x值=

ThreadB : 当前foo对象的x值=

ThreadB : 当前foo对象的x值=

ThreadA : 当前foo对象的x值=

ThreadA : 当前foo对象的x值=

ThreadB : 当前foo对象的x值=

Process finished with exit code

从结果发现这样的输出值明显是不合理的原因是两个线程不加控制的访问Foo对象并修改其数据所致

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

在具体的Java代码中需要完成一下两个操作

把竞争访问的资源类Foo变量x标识为private;

同步哪些修改变量的代码使用synchronized关键字同步方法或代码

同步和锁定

锁的原理

Java中每个对象都有一个内置锁

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

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

一个对象只有一个锁所以如果一个线程获得该锁就没有其他线程可以获得锁直到第一个线程释放(或返回)锁这也意味着任何其他线程都不能进入该对象上的synchronized方法或代码块直到该锁被释放

释放锁是指持锁线程退出了synchronized同步方法或代码块

关于锁和同步有一下几个要点

只能同步方法而不能同步变量和类

每个对象只有一个锁当提到同步时应该清楚在什么上同步?也就是说在哪个对象上同步?

不必同步类中所有的方法类可以同时拥有同步和非同步方法

如果两个线程要执行一个类中的synchronized方法并且两个线程使用相同的实例来调用方法那么一次只能有一个线程能够执行方法另一个需要等待直到锁被释放也就是说如果一个线程在对象上获得一个锁就没有任何其他线程可以进入(该对象的)类中的任何一个同步方法

如果线程拥有同步和非同步方法则非同步方法可以被多个线程自由访问而不受锁的限制

线程睡眠时它所持的任何锁都不会释放

线程可以获得多个锁比如在一个对象的同步方法里面调用另外一个对象的同步方法则获取了两个对象的同步锁

同步损害并发性应该尽可能缩小同步范围同步不但可以同步整个方法还可以同步方法中一部分代码块

在使用同步代码块时候应该指定在哪个对象上同步也就是说要获取哪个对象的锁例如

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;

}

}

效果是完全一样的

静态方法同步

要同步静态方法需要一个用于整个类对象的锁这个对象是就是这个类(XXXclass)

例如

public static synchronized int setName(String name){

Xxxname = name;

}

等价于

public static int setName(String name){

synchronized(Xxxclass){

Xxxname = name;

}

}

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

如果线程试图进入同步方法而其锁已经被占用则线程在该对象上被阻塞实质上线程进入该对象的的一种池中必须在哪里等待直到其锁被释放该线程再次变为可运行或运行为止

当考虑阻塞时一定要注意哪个对象正被用于锁定

调用同一个对象中非静态同步方法的线程将彼此阻塞如果是不同对象则每个线程有自己的对象的锁线程间彼此互不干预

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

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

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

何时需要同步

在多个线程同时访问互斥(可交换)数据时应该同步以保护数据确保两个线程不会同时修改更改它

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

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

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

线程安全类

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

即使是线程安全类也应该特别小心因为操作的线程是间仍然不一定安全

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

看个代码

public class NameList {

private List nameList = CollectionssynchronizedList(new LinkedList())

public void add(String name) {

nameListadd(name)

}

public String removeFirst() {

if (nameListsize() > ) {

return (String) nameListremove(

} else {

return null;

}

}

}

public class Test {

public static void main(String[] args) {

final NameList nl = new NameList()

nladd(aaa

class NameDropper extends Thread{

public void run(){

String name = nlremoveFirst()

Systemoutprintln(name)

}

}

Thread t = new NameDropper()

Thread t = new NameDropper()

tstart()

tstart()

}

}

虽然集合对象

private List nameList = CollectionssynchronizedList(new LinkedList())

是同步的但是程序还不是线程安全的

出现这种事件的原因是上例中一个线程操作列表过程中无法阻止另外一个线程对列表的其他操作

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

public class NameList {

private List nameList = CollectionssynchronizedList(new LinkedList())

public synchronized void add(String name) {

nameListadd(name)

}

public synchronized String removeFirst() {

if (nameListsize() > ) {

return (String) nameListremove(

} else {

return null;

}

}

}

这样当一个线程访问其中一个同步方法时其他线程只有等待

线程死锁

死锁对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 resourceBvalue + resourceAvalue;

}

}

}

public void write(int a int b) {

synchronized (resourceB) {

synchronized (resourceA) {

resourceAvalue = a;

resourceBvalue = b;

}

}

}

}

假设read()方法由一个线程启动write()方法由另外一个线程启动读线程将拥有resourceA锁写线程将拥有resourceB锁两者都坚持等待的话就出现死锁

实际上上面这个例子发生死锁的概率很小因为在代码内的某个点CPU必须从读线程切换到写线程所以死锁基本上不能发生

但是无论代码中发生死锁的概率有多小一旦发生死锁程序就死掉有一些设计方法能帮助避免死锁包括始终按照预定义的顺序获取锁这一策略已经超出SCJP的考试范围

线程同步小结

线程同步的目的是为了保护多个线程反问一个资源时对资源的破坏

线程同步方法是通过锁来实现每个对象都有切仅有一个锁这个锁与一个特定的对象关联线程一旦获取了对象锁其他访问该对象的线程就无法再访问该对象的其他同步方法

对于静态同步方法锁是针对这个类的锁对象是该类的Class对象静态和非静态方法的锁互不干预一个线程获得锁当在一个同步方法中访问另外对象上的同步方法时会获取这两个对象锁

对于同步要时刻清醒在哪个对象上同步这是关键

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

当多个线程等待一个对象锁时没有获取到锁的线程将发生阻塞

死锁是线程间相互等待锁锁造成的在实际中发生的概率非常的小真让你写个死锁程序不一定好使呵呵但是一旦程序发生死锁程序将死掉

               

上一篇:Java线程池在运行后的结果反查

下一篇:java的volatile与多线程