1.   多线程控制类

为了保证多线程的三个特性,Java引入了很多线程控制机制,下面介绍其中常用的几种:

l  ThreadLocal

l  原子类

l  Lock类

l  Volatile关键字

1.1. ThreadLocal

1.1.1.   作用

ThreadLocal提供线程局部变量,即为使用相同变量的每一个线程维护一个该变量的副本。

当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用ThreadLocal,比如数据库连接Connection,每个请求处理线程都需要,但又不相互影响,就是用ThreadLocal实现。

1.1.2.   示例

两个线程分别转账

package com.controller;

/**
* @Auther: lanhaifeng
* @Date: 2019/11/21 0021 10:09
* @Description:线程局部变量ThreadLocal
* @statement:
*/
public class ThreadLocaclDemo {
//1.创建银行对象:钱,取款,存款
static class Bank {
private ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue(){
return 0;
}
}; public Integer get(){
return threadLocal.get();
} public void set(Integer money){
threadLocal.set(threadLocal.get()+money);
}
}
//2.创建转账对象:从银行中取钱,转账,保存到帐户
static class Transfer implements Runnable{
private Bank bank; public Transfer(Bank bank){
this.bank = bank;
}
public void run() {
for(int i=0; i<10; i++){
bank.set(10);
System.out.println(Thread.currentThread().getName()+"帐户余额:"+bank.get());
}
}
}
//3.在main方法中使用两个对象模拟转账
public static void main(String[] args){
Bank bank = new Bank();
Transfer transfer = new Transfer(bank);
Thread thread1 = new Thread(transfer, "客户1");
Thread thread2 = new Thread(transfer, "客户2"); thread1.start();
thread2.start();
}
}

执行效果:

1.1.3.   分析

l  在ThreadLocal类中定义了一个ThreadLocalMap,

l  每一个Thread都有一个ThreadLocalMap类型的变量threadLocals

l  threadLocals内部有一个Entry,Entry的key是ThreadLocal对象实例,value就是共享变量副本

l  ThreadLocal的get方法就是根据ThreadLocal对象实例获取共享变量副本

l  ThreadLocal的set方法就是根据ThreadLocal对象实例保存共享变量副本

1.2. 原子类

Java的java.util.concurrent.atomic包里面提供了很多可以进行原子操作的类,分为以下四类:

l  原子更新基本类型:AtomicInteger、AtomicBoolean、AtomicLong

l  原子更新数组:AtomicIntegerArray、AtomicLongArray

l  原子更新引用:AtomicReference、AtomicStampedReference等

l  原子更新属性:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater

提供这些原子类的目的就是为了解决基本类型操作的非原子性导致在多线程并发情况下引发的问题。

1.2.1.   非原子性操作问题演示

非原子性的操作会引发什么问题呢?下面以i++为例演示非原子性操作问题。

i++并不是原子操作,而是由三个操作构成:

tp1 = i;
tp2 = tp1+1;
i = tp2;

所以单线程i的值不是有问题,但多线程下就会出错,多线程示例代码如下:

package com.multithread.thread;

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicClass {
static int n = 0;
public static void main(String[] args) throws InterruptedException {
int j = 0;
while(j<100){
n = 0;
            Thread t1 = new Thread(){
public void run(){
for(int i=0; i<1000; i++){
n++;
                    }
                }
            };
            Thread t2 = new Thread(){
public void run(){
for(int i=0; i<1000; i++){
n++;
                    }
                }
            };
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println("n的最终值是:"+n);
            j++;
        }     }
}

执行结果如下:发现n的最终值可能不是2000

1.2.2.   原子类解决非原子性操作问题

以上代码修改如下:

package com.multithread.thread;

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicClass {
static AtomicInteger n;
public static void main(String[] args) throws InterruptedException {
int j = 0;
while(j<100){
n = new AtomicInteger(0);
            Thread t1 = new Thread(){
public void run(){
for(int i=0; i<1000; i++){
n.getAndIncrement();
                    }
                }
            };
            Thread t2 = new Thread(){
public void run(){
for(int i=0; i<1000; i++){
n.getAndIncrement();
                    }
                }
            };
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println("n的最终值是:"+n);
            j++;
        }     }
}

执行结果如下:n的值永远是2000

1.2.3.   原子类CAS原理分析

1.2.4.   CAS的ABA问题及解决

1.2.4.1.       ABA问题分析

当前内存的值一开始是A,被另外一个线程先改为B然后再改为A,那么当前线程访问的时候发现是A,则认为它没有被其他线程访问过。在某些场景下这样是存在错误风险的。如下图:

1.2.4.2.       ABA问题解决

package com.multithread.thread;

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference; public class AtomicClass {
static AtomicStampedReference<Integer>n;
public static void main(String[] args) throws InterruptedException {
int j = 0;
while(j<100){
n = new AtomicStampedReference<Integer>(0,0);
            Thread t1 = new Thread(){
public void run(){
for(int i=0; i<1000; i++){
int stamp;
                        Integer reference;
do{
                            stamp = n.getStamp();
                            reference = n.getReference();
                        } while(!n.compareAndSet(reference, reference+1, stamp, stamp+1));
                    }
                }
            };
            Thread t2 = new Thread(){
public void run(){
for(int i=0; i<1000; i++){
int stamp;
                        Integer reference;
do{
                            stamp = n.getStamp();
                            reference = n.getReference();                         } while(!n.compareAndSet(reference, reference+1, stamp, stamp+1));
                    }
                }
            };
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println("n的最终值是:"+n.getReference());
            j++;
        }     }
}

执行效果如下:执行结果也是2000

注意:采用AtomicStampedReference会降低性能,慎用。

1.3. Lock类

1.3.1.   Lock接口关系图

Lock和ReadWriteLock是两大锁的根接口

Lock 接口支持重入、公平等的锁规则:实现类 ReentrantLock、ReadLock和WriteLock。
ReadWriteLock 接口定义读取者共享而写入者独占的锁,实现类:ReentrantReadWriteLock。

1.3.2.  
可重入锁

不可重入锁,即线程请求它已经拥有的锁时会阻塞。

可重入锁,即线程可以进入它已经拥有的锁的同步代码块。

publicclassReentrantLockTest {
 
public static void main(String[] args) throws InterruptedException {
 
        ReentrantLock lock = new ReentrantLock();
 
for (int i = 1; i <= 3; i++) {
lock.lock();
        }
 
for(int i=1;i<=3;i++){
try {
 
            } finally {
lock.unlock();
            }
        }
    }
}

1.3.3.   读写锁

读写锁,即可以同时读,读的时候不能写;不能同时写,写的时候不能读。

示例代码:

package com.multithread.thread;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock; /**
 *
读写操作类
 */
public class ReadWriteLockDemo { private Map<String, Object>map = new HashMap<String, Object>();
//创建一个读写锁实例
private ReadWriteLock rw = new ReentrantReadWriteLock();
//创建一个读锁
private Lock r = rw.readLock();
//创建一个写锁
private Lock w = rw.writeLock(); /**
     *
读操作
     *
     * @param
key
* @return
*/
public Object get(String key) {
r.lock();
        System.out.println(Thread.currentThread().getName() + "读操作开始执行......");
try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
try {
return map.get(key);
        } finally {
r.unlock();
            System.out.println(Thread.currentThread().getName() + "读操作执行完成......");
        }
    } /**
     *
写操作
     *
     * @param
key
* @param value
*/
public void put(String key, Object value) {
try {
w.lock();
            System.out.println(Thread.currentThread().getName() + "写操作开始执行......");
try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
map.put(key, value);
        } finally {
w.unlock();
            System.out.println(Thread.currentThread().getName() + "写操作执行完成......");
        }
    } public static void main(String[] args) {
final ReadWriteLockDemo d = new ReadWriteLockDemo();
        d.put("key1", "value1");
new Thread(new Runnable() {
public void run() {
d.get("key1");
            }
        }).start(); new Thread(new Runnable() {
public void run() {
d.get("key1");
            }
        }).start();
new Thread(new Runnable() {
public void run() {
d.get("key1");
            }
        }).start();
    } }

执行效果如下:写操作为独占锁,执行期间不能读;读操作可

1.4. Volatile关键字

1.4.1.   作用

一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

l  保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(注意:不保证原子性)

l  禁止进行指令重排序。(保证变量所在行的有序性)

当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

1.4.2.   应用场景

基于volatile的作用,使用volatile必须满足以下两个条件:

l  对变量的写操作不依赖于当前值

l  该变量没有包含在具有其他变量的不变式中

常见应用场景如下:

状态量标记:

volatilebooleanflag = false;

while(!flag){

    doSomething();

}

publicvoidsetFlag() {

    flag = true;

}

volatilebooleaninited = false;

//线程1:

context = loadContext(); 

inited = true;           

//线程2:

while(!inited ){

sleep()

}

doSomethingwithconfig(context);

双重校验:

classSingleton{

    privatevolatilestaticSingleton instance = null;

    privateSingleton() {

    }

    publicstaticSingleton getInstance() {

        if(instance==null) {

            synchronized(Singleton.class) {

                if(instance==null)

                    instance = newSingleton();

            }

        }

        returninstance;

    }

}

java多线程(六)线程控制类的更多相关文章

  1. Java多线程之线程其他类

    Java多线程之线程其他类 实际编码中除了前面讲到的常用的类之外,还有几个其他类也有可能用得到,这里来统一整理一下: 1,Callable接口和Future接口 JDK1.5以后提供了上面这2个接口, ...

  2. Java多线程(六) —— 线程并发库之并发容器

    参考文献: http://www.blogjava.net/xylz/archive/2010/07/19/326527.html 一.ConcurrentMap API 从这一节开始正式进入并发容器 ...

  3. Java多线程(六) 线程系列总结

    多线程系列终于终结得差不多,本人对该系列所做的总结大致如下: 线程锁模块耗费了大量的时间,底层的AQS实现比较复杂.仍然没有时间总结源码部分,能够坚持写下这么几个篇幅的内容真心佩服自己....希望继续 ...

  4. Java多线程之线程的控制

    Java多线程之线程的控制 线程中的7 种非常重要的状态:  初始New.可运行Runnable.运行Running.阻塞Blocked.锁池lock_pool.等待队列wait_pool.结束Dea ...

  5. Java多线程父子线程关系 多线程中篇(六)

    有的时候对于Java多线程,我们会听到“父线程.子线程”的概念. 严格的说,Java中不存在实质上的父子关系 没有方法可以获取一个线程的父线程,也没有方法可以获取一个线程所有的子线程 子线程的消亡与父 ...

  6. Java多线程之线程协作

    Java多线程之线程协作 一.前言 上一节提到,如果有一个线程正在运行synchronized 方法,那么其他线程就无法再运行这个方法了.这就是简单的互斥处理. 假如我们现在想执行更加精确的控制,而不 ...

  7. Java多线程之线程的通信

    Java多线程之线程的通信 在总结多线程通信前先介绍一个概念:锁池.线程因为未拿到锁标记而发生的阻塞不同于前面五个基本状态中的阻塞,称为锁池.每个对象都有自己的锁池的空间,用于放置等待运行的线程.这些 ...

  8. java多线程与线程间通信

    转自(http://blog.csdn.net/jerrying0203/article/details/45563947) 本文学习并总结java多线程与线程间通信的原理和方法,内容涉及java线程 ...

  9. Java多线程之线程的暂停

    Java多线程之线程的暂停 下面该稍微休息一下了呢……不过,这里说的是线程休息,不是我们哦.本节将介绍一下让线程暂停运行的方法. 线程Thread 类中的sleep 方法能够暂停线程运行,Sleep ...

  10. Java多线程与线程池技术

    一.序言 Java多线程编程线程池被广泛使用,甚至成为了标配. 线程池本质是池化技术的应用,和连接池类似,创建连接与关闭连接属于耗时操作,创建线程与销毁线程也属于重操作,为了提高效率,先提前创建好一批 ...

随机推荐

  1. Nodejs入门级

    应用不同模块分析 我们来分解一下这个应用,为了实现一个应用,我们需要实现哪些部分呢? 我们需要提供Web页面,因此需要一个HTTP服务器 对于不同的请求,根据请求的URL,我们的服务器需要给予不同的响 ...

  2. Jmeter 使用badyboy 录制脚本

    在使用jemeter 做测试时,一般需要先录制脚本,这个过程可以通过手工添加采样器进行采样. 比较方便的方法是使用badboy 来录制脚本录制完脚本后使用jemeter 再做压力测试,大大加快测试进度 ...

  3. Vue开发之项目创建

    1.编辑器配置 习惯使用VScode进行开发时,可以安装EditorConfig for Visual Studio Code插件,然后在项目中新建.editorconfig文件,来配置编辑器的使用习 ...

  4. PAT 乙级 1019.数字黑洞 C++/Java

    题目来源 给定任一个各位数字不完全相同的 4 位正整数,如果我们先把 4 个数字按非递增排序,再按非递减排序,然后用第 1 个数字减第 2 个数字,将得到一个新的数字.一直重复这样做,我们很快会停在有 ...

  5. P1850 换教室[dp+期望]

    流下了不会概率的眼泪,由于不会概率,转移少写了点东西... 这个dp很简单,就是一个普通的线性dp加点期望.(刚开始写这道题时信笔写下) \(dp[0/1][i][j]\)表示到第\(i\)个时间段时 ...

  6. Twitter Storm学习笔记

    官方英文文档:http://storm.apache.org/documentation/Documentation.html 本文是学习笔记,转载整合加翻译,主要是为了便于学习. 一.基本概念 参考 ...

  7. laravel5.1框架基础之Blade模板继承简单使用方法分析

    本文实例讲述了laravel5.1框架基础之Blade模板继承简单使用方法.分享给大家供大家参考,具体如下: 模板继承什么用? 自然是增强基础页面的复用,有利于页面文档的条理,也便于更改多处使用的内容 ...

  8. MapReduce内存调优

    内存调优 Hadoop处理数据时,出现内存溢出的处理方法?(内存调优) 1.Mapper/Reducer阶段JVM内存溢出(一般都是堆) 1)JVM堆(Heap)内存溢出:堆内存不足时,一般会抛出如下 ...

  9. 重构之字段改名 UML行为图 用例图 时序图&协作图 状态图&活动图 依恋情结

    简单的使用一下字段改名 为什么使用字段改名: ​ 你在一个软件上做的工作越多,对这个软件的数据的理解就越深刻,你需要把这些理解融入到代码中.利用名字的解释作用,让代码更容易被理解. 如何找到该变量的所 ...

  10. render()--组件--纯函数

    render() 当 被调用时,它会检查 和 的变化并返回以下类型之一: React 元素.通常通过 JSX 创建.例如,<div /> 会被 React 渲染为 DOM 节点,<M ...