Java多线程(三)
线程同步
- 资源共享
如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。 - 线程同步
-
概念
1.多线程共享数据时,会发生线程不安全的情况(多个线程同时操作一个可共享的资源变量),多线程共享数据必须同步。
2.同步是一种高开销的操作,因此应该尽量减少同步的内容。
3.线程同步方法是通过锁来实现,每个对象都有切仅有一个锁,这个锁与一个特定的对象关联,线程一旦获取了对象锁,其他访问该对象的线程就无法再访问该对象的其他非同步方法。
4.对于静态同步方法,锁是针对这个类的,锁对象是该类的Class对象。静态和非静态方法的锁互不干预。一个线程获得锁,当在一个同步方法中访问另外对象上的同步方法时,会获取这两个对象锁。
5.对于同步,要时刻清醒在哪个对象上同步,这是关键。
6.编写线程安全的类,需要时刻注意对多个线程竞争访问资源的逻辑和安全做出正确的判断,对“原子”操作做出分析,并保证原子操作期间别的线程无法访问竞争资源。
7.当多个线程等待一个对象锁时,没有获取到锁的线程将发生阻塞。
8.死锁是线程间相互等待锁锁造成的,在实际中发生的概率非常的小。
9.需要使用线程同步的根本原因在于对普通变量的操作不是原子的。 -
同步方法
-
synchronized关键字修饰方法;public synchronized void save(){}
-
synchronized关键字修饰的语句块。synchronized(object){}
-
使用特殊域变量(volatile)实现线程同步,eg:
class Bank {
//需要同步的变量加上volatile
private volatile int account = 100;public int getAccount() { return account; } //这里不再需要synchronized public void save(int money) { account += money; } }
-
使用重入锁实现线程同步
ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力class Bank { private int account = 100; //需要声明这个锁 private Lock lock = new ReentrantLock(); public int getAccount() { return account; } //这里不再需要synchronized public void save(int money) { lock.lock(); try{ account += money; }finally{ lock.unlock(); } } }```
-
使用局部变量实现线程同步
如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。public class Bank{ //使用ThreadLocal类管理共享变量account private static ThreadLocal<Integer> account = new ThreadLocal<Integer>(){ @Override protected Integer initialValue(){ return 100; } }; public void save(int money){ account.set(account.get()+money); } public int getAccount(){ return account.get(); } }
-
使用阻塞队列实现线程同步
前面5种同步方式都是在底层实现的线程同步,但是我们在实际开发当中,应当尽量远离底层结构。 使用javaSE5.0版本中新增的java.util.concurrent包将有助于简化开发。 可以使用LinkedBlockingQueue来实现线程的同步 。LinkedBlockingQueue是一个基于已连接节点的,范围任意的blocking queue。 队列是先进先出的顺序(FIFO)public class BlockingSynchronizedThread { /** * 定义一个阻塞队列用来存储生产出来的商品 */ private LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<Integer>(); private static final int size = 10; /** * 定义启动线程的标志,为0时,启动生产商品的线程;为1时 , * 启动消费商品的线程 */ private int flag = 0; private class LinkBlockThread implements Runnable { @Override public void run() { int new_flag = flag++; System.out.println("启动线程 " + new_flag); if (new_flag == 0) { for (int i = 0; i < size; i++) { int b = new Random().nextInt(255); System.out.println("生产商品:" + b + "号"); try { queue.put(b); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("仓库中还有商品:" + queue.size() + "个"); try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } else { for (int i = 0; i < size / 2; i++) { try { int n = queue.take(); System.out.println("消费者买去了" + n + "号商品"); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("仓库中还有商品:" + queue.size() + "个"); try { Thread.sleep(100); } catch (Exception e) { // TODO: handle exception } } } } } public static void main(String[] args) { BlockingSynchronizedThread bst = new BlockingSynchronizedThread(); LinkBlockThread lbt = bst.new LinkBlockThread(); Thread thread1 = new Thread(lbt); Thread thread2 = new Thread(lbt); thread1.start(); thread2.start(); } }
-
使用原子变量实现线程同步
java的util.concurrent.atomic包中提供了创建了原子类型变量的工具类,使用该类可以简化线程同步。
其中AtomicInteger 表可以用原子方式更新int的值,可用在应用程序中(如以原子方式增加的计数器),但不能用于替换Integer;可扩展Number,允许那些处理机遇数字类的工具和实用工具进行统一访问。class Bank { private AtomicInteger account = new AtomicInteger(100); public AtomicInteger getAccount() { return account; } public void save(int money) { account.addAndGet(money); } }
-
-
synchronized关键字
1.概念:
由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步.
2.作用域:某个对象实例内,某个类范围
3.用法:1.synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,它可以对类的所有对象实例起作用。 2.通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。它的作用域是当前对象; 3.如果一个对象有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,其它线程不能同时访问这个对象中任何一个synchronized方法。 这时,不同的对象实例的synchronized方法是不相干扰的。也就是说,其它线程照样可以同时访问相同类的另一个对象实例中的synchronized方法; 4.synchronized关键字是不能继承的,也就是说,基类的方法synchronized f(){} 在继承类中并不自动是synchronized f(){},而是变成了f(){}。 继承类需要你显式的指定它的某个方法为synchronized方法; 5.无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁――而且同步方法很可能还会被其他线程的对象访问。 6.每个对象只有一个锁(lock)与之相关联。 7.把synchronized当作函数修饰符时,这时synchronized锁定的是哪个对象呢?它锁定的是调用这个同步方法对象。 当一个对象P1在不同的线程中执行这个同步方法时,它们之间会形成互斥,达到同步的效果。但是这个对象所属的Class所产生的另一对象P2却可以任意调用这个被加了synchronized关键字的方法。 8.synchronized修饰代码块时,如public void method3(SomeObject so){ synchronized(so){ 这时,锁就是so这个对象,谁拿到这个锁谁就可以运行它所控制的那段代码。 当有一个明确的对象作为锁时,就可以这样写程序,但当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的instance变量(它得是一个对象)来充当锁: class Foo implements Runnable{ private byte[] lock = new byte[0]; // 特殊的instance变量 Public void methodA(){ synchronized(lock) { //… }}} 零长度的byte数组对象创建起来将比任何对象都经济 9.如果一个类中定义了一个synchronized的static函数A,也定义了一个synchronized 的instance函数B,那么这个类的同一对象Obj在多线程中分别访问A和B两个方法时,不会构成同步,因为它们的锁都不一样。 A方法的锁是Obj这个对象,而B的锁是Obj所属的那个Class。 eg: Class Foo{ public synchronized static void methodAAA() // 同步的static 函数 {} public void methodBBB(){ synchronized(Foo.class) // class literal(类名称字面常量) }} 10.Synchronized与lock的异同? 相同:二者都可以解决线程安全问题 不同:synchronized机制在执行完相应的代码逻辑以后,自动的释放同步监视器,lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())(同时以为着lock的方式更为灵活) 优先使用顺序: LOCK-》同步代码块-》同步方法
-
volatile
1.概念:
volatile关键字为域变量的访问提供了一种免锁机制,使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,因此每次使用该域就要重新计算,而不是使用寄存器中的值,volatile不会提供任何原子操作,它也不能用来修饰final类型的变量
2.用法多线程中的非同步问题主要出现在对域的读写上,如果让域自身避免这个问题,则就不需要修改操作该域的方法。 用final域,有锁保护的域和volatile域可以避免非同步的问题。
-
ReenreantLock类
1.ReentrantLock() : 创建一个ReentrantLock实例 lock() : 获得锁
unlock() : 释放锁
2.ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用
3.如果synchronized关键字能满足用户的需求,就用synchronized,因为它能简化代码;如果需要更高级的功能,就用ReentrantLock类,此时要注意及时释放锁,否则会出现死锁,通常在finally代码释放锁 -
ThreadLocal 类
1.ThreadLocal() : 创建一个线程本地变量
2.get() : 返回此线程局部变量的当前线程副本中的值
3.initialValue() : 返回此线程局部变量的当前线程的"初始值"
4.set(T value) : 将此线程局部变量的当前线程副本中的值设置为value
5.ThreadLocal与同步机制:ThreadLocal与同步机制都是为了解决多线程中相同变量的访问冲突问题。前者采用以"空间换时间"的方法,后者采用以"时间换空间"的方式 -
BlockingQueue
1.定义了阻塞队列的常用方法,尤其是三种添加元素的方法,我们要多加注意,当队列满时:
add()方法会抛出异常
offer()方法返回false
put()方法会阻塞
2.LinkedBlockingQueue 类常用方法
LinkedBlockingQueue() : 创建一个容量为Integer.MAX_VALUE的LinkedBlockingQueue
put(E e) : 在队尾添加一个元素,如果队列满则阻塞
size() : 返回队列中的元素个数
take() : 移除并返回队头元素,如果队列空则阻塞 -
AtomicInteger
AtomicInteger(int initialValue) : 创建具有给定初始值的新的AtomicInteger
addAddGet(int dalta) : 以原子方式将给定值与当前值相加
get() : 获取当前值
原子操作主要有:
对于引用变量和大多数原始变量(long和double除外)的读写操作;
对于所有使用volatile修饰的变量(包括long和double)的读写操作。
-
- 线程池同步
ExecutorService taskExecutor = Executors.newFixedThreadPool(4); while(...) { taskExecutor.execute(new MyTask()); } taskExecutor.shutdown(); try { taskExecutor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS); } catch (InterruptedException e) { ... }
- 参考文章
https://www.cnblogs.com/zxfei/p/10913497.html
https://www.cnblogs.com/XHJT/p/3897440.html
https://blog.csdn.net/gf771115/article/details/51682561