Java多线程总结之---线程同步

一、意义
java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证了该变量的唯一性和准确性。
在具体的Java代码中需要完成以下两个操作:

把竞争访问的资源类变量标识为private;不让外部直接更改

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

二、锁的原理

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

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

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

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

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

关于锁和同步,有一下几个要点:
1)、只能同步方法,而不能同步变量和类;
2)、每个对象只有一个锁;当提到同步时,应该清楚在哪个对象上同步
3)、不必同步类中所有的方法,类可以同时拥有同步和非同步方法。同步方法的访问会受到锁的限制,非同步方法可以被多个线程自由访问而不受锁的限制。
4)、如果一个线程在对象上获得一个锁,就没有任何其他线程可以进入(该对象的)类中的任何一个同步方法。
5)、线程睡眠时,它所持的任何锁都不会释放。
6)、线程可以获得多个锁。比如,在一个对象的同步方法里面调用另外一个对象的同步方法,则获取了两个对象的同步锁。
7)、同步损害并发性,应该尽可能缩小同步范围。同步不但可以同步整个方法,还可以同步方法中一部分代码块。

三、线程同步的解决方法

1、synchronized关键字实现同步

即有synchronized关键字修饰的方法。由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。synchronized关键字加锁的原理,其实是对对象加锁,不论你是在方法前加synchronized还是语句块前加,锁住的都是对象整体

public synchronized int getX() {  
    return x++;  
}

当然,同步方法也可以改写为非同步方法,但功能完全一样的,例如:

public int getX() {  
      synchronized (this) {  
          return x;  
      }  
  } 

synchronized也可以修饰静态方法,同步静态方法,需要一个用于整个类对象的锁,这个对象是就是这个类(XXX.class)

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

等价于:

public static String getName(){  
      synchronized(Xxx.class){  
            return Xxx.name;  
      }  
}  

2、使用特殊域变量(volatile)实现线程同步

volatile关键字为域变量的访问提供了一种免锁机制;
 • 使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新;
 • 每次使用它都到主存中进行读取。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。volatile关键字就是提示JVM:对于这个成员变量不能保存它的私有拷贝,而应直接与共享成员变量交互。
 • volatile保证了可见性和有序性,但不会提供任何原子操作,它也不能用来修饰final类型的变量
 • volatile最适用一个线程写,多个线程读的场合。如果有多个线程并发写操作,仍然需要使用锁或者线程安全的容器或者原子变量来代替。

这篇博文讲解的比较清楚:Java之美之线程同步的引入
您只能在有限的一些情形下使用 volatile 变量替代锁。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:
1)、对变量的写操作不依赖于当前值。
2)、该变量没有包含在具有其他变量的不变式中。

也就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。更多信息参考以下博文:Java并发编程:volatile关键字解析

:结合使用 volatile 和 synchronized 实现 “开销较低的读-写锁”
volatile 允许多个线程执行读操作,因此当使用 volatile 保证读代码路径时,要比使用锁执行全部代码路径获得更高的共享度 —— 就像读-写操作一样。

public class CheesyCounter {
    private volatile int value;  
    //读操作,没有synchronized,提高性能  
    public int getValue() {   
        return value;   
    }   
    //写操作,必须synchronized。因为x++不是原子操作  
    public synchronized int increment() {  
        return value++;  
    }  

注:多线程中的非同步问题主要出现在对域的读写上,如果让域自身避免这个问题,则就不需要修改操作该域的方法。用final域,有锁保护的域volatile域可以避免非同步的问题。

3、使用重入锁(Lock)实现线程同步
在Java5中,专门提供了锁对象,利用锁可以方便的实现资源的封锁,用来控制对竞争资源并发访问的控制,这些内容主要集中在java.util.concurrent.locks 包下面,里面有三个重要的接口Condition、Lock、ReadWriteLock。

特性
ConditionCondition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set (wait-set)。
LockLock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。
ReadWriteLockReadWriteLock 维护了一对相关的锁定,一个用于只读操作,另一个用于写入操作。

ReentrantLock类是可重入、互斥、实现了Lock接口的锁。
重进入是一种基于per-thread的机制,并不是一种独立的同步方法 。基本实现是这样的:

每个锁关联一个请求计数器和一个占有它的线程,当计数器为0时,锁是未被占有的,线程请求时,JVM将记录锁的占有者,并将计数器增1,当同一线程再次请求这个锁时,计数器递增;线程退出时,计数器减1,直到计数器为0时,锁被释放。

class Bank {
        private int account = 100;
        //创建一个ReentrantLock实例
        private Lock lock = new ReentrantLock();
        public int getAccount() {
            return account;
        }
        //这里不再需要synchronized
        public void save(int money) {
            lock.lock();//获得锁
            try {
                account += money;
            } finally {
                lock.unlock();//释放锁
            }
        }
    }

注意:lock一定要显示的unlock(),不然会出现问题

4、Semaphore(信号量)

信号量的意思就是设置一个最大值,来控制有限个对象同时对资源进行访问。因为有的时候有些资源并不是只能由一个线程同时访问的,举个例子,我这儿有5个碗,只能满足5个人同时用餐,那么我可以设置一个最大值5,线程访问时,用acquire() 获取一个许可,如果没有就等待,用完时用release() 释放一个许可。这样就保证了最多5个人同时用餐,不会造成安全问题,这是一种很简单的同步机制。

 public static void main(String[] args) {
        // 线程池
        ExecutorService exec = Executors.newCachedThreadPool();
        // 只能5个线程同时访问
        final Semaphore semp = new Semaphore(5);
        // 模拟20个客户端访问
        for (int index = 0; index < 20; index++) {
            final int NO = index;
            Runnable run = new Runnable() {
                public void run() {
                    try {
                        // 获取许可
                        semp.acquire();
                        System.out.println("Accessing: " + NO);
                        Thread.sleep((long) (Math.random() * 10000));
                        // 访问完后,释放
                        semp.release();
                        System.out.println("-----------------"+semp.availablePermits());
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
            exec.execute(run);
        }
        // 退出线程池
        exec.shutdown();
    }

当信号量的数量上限是1时,Semaphore可以被当做锁来使用。通过take和release方法来保护关键区域。这就有点类似于lock的功能了,只不过信号灯量可以由一个线程使用,然后由另一个线程来进行释放,而锁只能由同一个线程启动和释放,

5、事件驱动
事件驱动的意思就是一件事情办完后,唤醒其它线程去干另一件。这样就保证:1、数据可见性。在A线程执行的时候,B线程处于睡眠状态,不可能对共享变量进行修改。2、互斥性。相当于上锁,不会有其它线程干扰。常用的方法有:sleep()、wait()、notify()等等。

四、线程同步的问题

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

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

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

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

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

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

五、何时需要同步

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

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

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

如果需要在非静态方法中使用静态字段,或者在静态字段中调用非静态方法,问题将变得非常复杂。

六、Java中封装好的同步和并发工具类

1)、同步容器

Java常用的容器有ArrayList、LinkedList、HashMap等等,这些容器都是非线程安全的。如果有多个线程并发地访问这些容器时,就会出现问题。
所以,Java提供了同步容器供用户使用。在Java中,同步容器主要包括2类:
A、Vector、Stack、HashTable
Vector实现了List接口,Vector实际上就是一个数组,和ArrayList类似,但是Vector中的方法都是synchronized方法,即进行了同步措施。
Stack也是一个同步容器,它的方法也用synchronized进行了同步,它实际上是继承于Vector类。
HashTable实现了Map接口,它和HashMap很相似,但是HashTable进行了同步处理,而HashMap没有。
B、Collections类中提供的静态工厂方法创建的类
Collections类是一个工具提供类。在Collections类中提供了大量的方法,比如对集合或者容器进行排序、查找等操作。更重要的是,在它里面提供了几个静态工厂方法来创建同步容器类
从同步容器的具体实现源码可知,同步容器中的方法采用了synchronized进行了同步,那么必然会影响到程序的执行性能。于是Java提供了性能更优的并发容器

注:我们可以通过Collections的一些方法,使得HashMap这样的非线程安全类变为线程安全的类,如:Collections.synchronizedMap(new HashMap())

2)、并发容器
java.util.concurrent提供了多种并发容器,总体上来说有4类:
Queue类型的BlockingQueue和ConcurrentLinkedQueue
Map类型的ConcurrentMap
Set类型的ConcurrentSkipListSet和CopyOnWriteArraySet
List类型的CopyOnWriteArrayList

3)、原子量

所谓的原子量即操作变量的操作是“原子的”,该操作不可再分,因此是线程安全的。

为何要使用原子变量呢,原因是多个线程对单个变量操作也会引起一些问题。在Java5之前,可以通过volatile、synchronized关键字来解决并发访问的安全问题,但这样太麻烦。

Java5之后,专门提供了用来进行单变量多线程并发安全访问的工具包java.util.concurrent.atomic,其中的类也很简单。
这里写图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值