JUC线程框架深度解析-01、juc基础

JUC线程框架深度解析-juc基础

01、JUC基础使用

一、JUC开发包简介

在这里插入图片描述

【 java.util.concurrent开发包 】
➣ 传统线程编程模型之中为防止死锁等现象的出现(wait()、notify()、synchronized)时往往会考虑性能、公平性、资源管理等问题,这样加重了程序开发人员的负担;
➣ Java5.0添加了一个新的java.util.concurrent开发包(简称JUC)。
利用此包进行的多线程编程将有效的减少竞争条件(race conditions)和死锁线程。

【 java.util.concurrent核心类 】
1)Executor:具有Runnable任务的执行者。
2)ExecutorService:一个线程池管理者,其实现类有多种,我会介绍一部分,我们能把Runnable,Callable提交到池中让其调度。
3)Semaphore:一个计数信号量。
4)ReentrantLock:一个可重入的互斥锁定Lock,功能类似synchronized,但要强大的多。
5)Future:是与Runnable,Callable进行交互的接口,比如一个线程执行结束后取返回的结果等,还提供了cancel终止线程。
6)BlockingQueue:阻塞队列。
7)CompletionService:ExecutorService的扩展,可以获得线程执行结果的。
8)CountDownLatch:一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。
9)CyclicBarrier:一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点。
10)Future:表示异步计算的结果。
11)ScheduldExecutorService:一个ExecutorService,可安排在给定的延迟后运行或定期执行的命令。

二、TimeUnit工具类

在java.util.concurrent开发包里面提供有一个TimeUnit类, 时间单元类。该类是一个枚举类型,这也是juc开发包里面唯一的一个枚举类。

public enum TimeUnit{
    
    
    ....
}

这个类之中支持有:日、时、分、秒、毫秒、微妙、纳秒,这些是更加精准的控制单位。
**范例:**如果现在要求你进行休眠控制,休眠2秒
• 休眠的处理只有Thread.sleep()方法。

public static void main(String[] args) throws Exception {
    
    
    System.out.println("【START】" + System.currentTimeMillis());
    Thread.sleep(2 * 1000);    // 需要自己根据毫秒来进行计算
    System.out.println("【END】" + System.currentTimeMillis());
}

那么这个时候对于以上的计算可以直接利用TimeUnit做准确处理。
范例:休眠2秒
TimeUnit.SECONDS.sleep(2); // 休眠2秒
在TimeUnit里面最为重要的特点是可以方便的进行各种时间单位的转换,它提供了一个convert()方法。
范例:实现转换处理

public long convert(long sourceDuration, TimeUnit sourceUnit) {
    
    
    throw new AbstractMethodError();
}
———————
public static void main(String[] args) throws Exception {
    
    
    long time = TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS) ;
    System.out.println("一小时转为毫秒:" + time);
}

一小时转为毫秒:3600000
**范例:**想知道3天后的日期

public static void main(String[] args) throws Exception {
    
    
    long time = TimeUnit.MILLISECONDS.convert(3, TimeUnit.DAYS);
    System.out.println("3天转为毫秒:" + time);
    // 当前时间的毫秒数 + 三天后的毫秒数
    long threeTime = System.currentTimeMillis() + time;
    System.out.println("3天后的日期:" + new Date(threeTime));
    System.out.println("3天后的日期:" + new SimpleDateFormat(
            "yyyy-MM-dd").format(new Date(threeTime)));
}

3天转为毫秒:259200000
3天后的日期:Sat Jan 18 14:47:25 CST 2020
3天后的日期:2020-01-18
如果不使用这样的处理操作,则这个时间间隔的计算将会非常麻烦,需要各种乘法计算。

三、原子操作类

查看如下代码输出的结果是多少?

package com.hanker.juc;
public class ThreadDemo {
    
    
    private int x = 0;

    private void count() {
    
    
        x++;
    }

    public void runTest() {
    
    
        new Thread() {
    
    
            @Override
            public void run() {
    
    
                for (int i = 0; i < 10000; i++) {
    
    
                    count();
                }
                System.out.println("final x from 1: " + x);
            }
        }.start();
        new Thread() {
    
    
            @Override
            public void run() {
    
    
                for (int i = 0; i < 10000; i++) {
    
    
                    count();
                }
                System.out.println("final x from 2: " + x);
            }
        }.start();
    }

    public static void main(String[] args) {
    
    
        new ThreadDemo().runTest();
    }
}

在这里插入图片描述

代码解释:
在这里插入图片描述

如何解决问题?
解决办法1:volatile 关键字

package com.hanker.juc;

/*
 * 一、volatile 关键字:当多个线程进行操作共享数据时,可以保证内存中的数据可见。
 * 					  相较于 synchronized 是一种较为轻量级的同步策略。
 * 
 * 注意:
 * 1. volatile 不具备“互斥性”
 * 2. volatile 不能保证变量的“原子性”
 */
public class TestVolatile {
    
    
	
	public static void main(String[] args) {
    
    
		ThreadDemo1 td = new ThreadDemo1();
		new Thread(td).start();
		
		while(true){
    
    
			if(td.isFlag()){
    
    
				System.out.println("------------------");
				break;
			}
		}
	}
}

class ThreadDemo1 implements Runnable {
    
    
	private  boolean flag = false; //volatile关键字
	@Override
	public void run() {
    
    
		try {
    
    
			Thread.sleep(200);
		} catch (InterruptedException e) {
    
    
		}
		flag = true;
		System.out.println("flag=" + isFlag());
	}
	public boolean isFlag() {
    
    
		return flag;
	}
	public void setFlag(boolean flag) {
    
    
		this.flag = flag;
	}
}

代码说明:不使用volatile关键字出现死循环,使用volatile关键字强制从主内存中读取数据解决问题。
解决办法2:原子类

package com.hanker.juc;

import java.util.concurrent.atomic.AtomicInteger;

public class ThreadDemo {
    
    
    private  AtomicInteger x = new AtomicInteger(0);

    private  void count() {
    
    
        x.incrementAndGet();
    }

    public void runTest() {
    
    
        new Thread() {
    
    
            @Override
            public void run() {
    
    
                for (int i = 0; i < 10000; i++) {
    
    
                    count();
                }
                System.out.println("final x from 1: " + x);
            }
        }.start();

        new Thread() {
    
    
            @Override
            public void run() {
    
    
                for (int i = 0; i < 10000; i++) {
    
    
                    count();
                }
                System.out.println("final x from 2: " + x);
            }
        }.start();
    }

    public static void main(String[] args) {
    
    
        new ThreadDemo().runTest();
    }
}

最后输出是20000.

  1. 内存可见性
    内存可见性(Memory Visibility)是指当某个线程正在使用对象状态而另一个线程在同时修改该状态,需要确保当一个线程修改了对象状态后,其他线程能够看到发生的状态变化。
    可见性错误是指当读操作与写操作在不同的线程中执行时,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情。
    我们可以通过同步来保证对象被安全地发布。除此之外我们也可以使用一种更加轻量级的 volatile变量。
  2. volatile 关键字
    Java 提供了一种稍弱的同步机制,即 volatile 变量,用来确保将变量的更新操作通知到其他线程。可以将 volatile 看做一个轻量级的锁,但是又与锁有些不同:

对于多线程,不是一种互斥关系;不能保证变量状态的“原子性操作”

  • 原子性操作解释
    例如 i++; 这个操作,它不是一个原子性操作,在实际执行时需要三步操作“读-改-写”:
int temp = i;
temp = temp + 1;
i = temp; 

在这里插入图片描述

在juc里面提供有一个atomic子包,这个自包里面保存的都是原子性的操作数据,也就是说这个包里所包含的数据类型前都使用volatile进行声明。

【 原子操作分类 】
➣ 原子操作,是指操作过程不会被中断,保证数据操作是以原子方式进行的;
• 基本类型:AtomicInteger, AtomicLong, AtomicBoolean.
• 数组类型:AtomicIntegerArray, AtomicLongArray.
• 引用类型:AtomicReference, AtomicStampedRerence.
• 对象的属性修改类型:AtomicIntegerFieldUpdater, AtomicLongFieldUpdater, AtomicReferenceFieldUpdater.

AtomicBoolean AtomicReference
AtomicInteger AtomicReferenceArray
AtomicIntegerArray AtomicReferenceFieldUpdater
AtomicIntegerFieldUpdater AtomicStampedReference
AtomicLong DoubleAccumulator
AtomicLongArray DoubleAdder
AtomicLongFieldUpdater LongAccumulator
AtomicMarkableReference LongAdder

范例:观察“AtomicLong”类型
实际上使用 AtomicLong 的时候里面包含有的就是”private volatile long value;”

public static void main(String[] args) throws Exception {
    
    
    AtomicLong num = new AtomicLong(100) ; // 设置为原子性操作
    System.out.println("数据自增:" + num.incrementAndGet());   // 101
    System.out.println("数减自增:" + num.decrementAndGet());   // 100
}

**范例:**实现基础数学运算

public static void main(String[] args) throws Exception {
    
    
    AtomicLong num = new AtomicLong(100);    // 设置为原子性操作
    System.out.println("加法操作:" + num.addAndGet(100));  // 200
    System.out.println("减法操作:" + num.addAndGet(-10));  // 190
}

毕竟这种操作不是原始的基本数据类型,它的操作时刻需要保证数据在多线程访问下的并发安全性,对于原子性的处理操作,以上并不是它的重点,只是它的操作形式,这里面最为重要的一个方法:

 CAS方法:public final boolean compareAndSet(long expect, long update);

范例:观察CAS的方法使用

public static void main(String[] args) throws Exception {
    
    
    AtomicLong num = new AtomicLong(100) ; // 设置为原子性操作
    // 如果现在要进行修改的内容是100,即:原始的原子类型里面为100,则使用333替换num的内容
    // 比较的值等于100,返回true   
    System.out.println(num.compareAndSet(100, 333));
    System.out.println(num); // 结果:333
}

设置数组的长度:public AtomicLongArray(int length),动态开辟;
设置具体的数组内容:public AtomicLongArray(long[] array),静态开辟;

**范例:**进行数组操作

public static void main(String[] args) throws Exception {
    
    
    AtomicLongArray array = new AtomicLongArray(new long[]{
    
    1, 2, 3});
    array.set(0, 99); // 原子性的数组必须使用set修改内容
    System.out.println(array);  // 输出结果:[99, 2, 3]
}

**范例:**使用原子性进行对象的描述

package com.hanker.juc;
import java.util.concurrent.atomic.AtomicReference;
 
public class TestCASDemo {
    
    
    public static void main(String[] args) throws Exception {
    
    
        AtomicReference<Member> ref = new AtomicReference<Member>();
        Member memA = new Member("张三", 20);
        Member memB = new Member("李四", 30);
        ref.set(memA);
        // 对象引用变更只得依靠地址比较“==”
        ref.compareAndSet(memA, memB);
        System.out.println(ref);  // 结果:name = 李四、age = 30
    }
}
 
class Member {
    
    
    private String name;
    private int age;
    public Member(String name, int age) {
    
    
        this.name = name;   this.age = age;
    }
    @Override
    public String toString() {
    
    
        return "name = " + this.name + "、age = " + this.age;
    }
}

以上集中类型严格来讲都算是常用的几种处理形式,但有一个问题,即:你可能本身类中定义的类型不是AtomicLong类型,那么可以用AtomicLongFieldUpdater类完成处理。
获得对象: public static AtomicLongFieldUpdater newUpdater(Class tclass, String fieldName);

范例:使用AtomicLongFieldUpdater更新器

import java.util.concurrent.atomic.AtomicLongFieldUpdater;
 
public class MLDNTestDemo {
    
    
    public static void main(String[] args) throws Exception {
    
    
        Book book = new Book(100000001,"Java开发实战") ;
        book.setBid(200000003); // 修改bid
        System.out.println(book);
    }
}
class Book {
    
    
    private volatile long bid ;    // 必须追加volatile关键字
    private String title ;
    public Book(long bid,String title) {
    
    
        this.bid = bid ;
        this.title = title ;
    }
    @SuppressWarnings({
    
     "rawtypes", "unchecked" })
    public void setBid(long bid) {
    
    
        AtomicLongFieldUpdater updater = AtomicLongFieldUpdater.newUpdater(
                super.getClass(), "bid") ;
        // 使用cas方法进行内容的修改
        updater.compareAndSet(this, this.bid, bid) ;
    }
    @Override
    public String toString() {
    
    
        return "图书编号:" + this.bid + "、名称:" + this.title;
    }
}     //输出结果:图书编号:200000003、名称:Java开发实战

四、ThreadFactory线程工厂类

在默认情况下如果要想创建一个线程类对象,大部分情况我们的选择,直接通过子类为父接口进行实例化,利用一个Runnable子类为Runnable接口实例化,或者直接利用Lambda表达式进行处理,不过在多线程运行机制里面考虑到线程对象创建的合理性,专门提供有一个ThreadFactory程序类,这是个工厂类,观察ThreadFactory类的定义:
public interface ThreadFactory {
public Thread newThread(Runnable r);
}
在这里插入图片描述

**范例:**使用ThreadFactory程序类

import java.util.concurrent.ThreadFactory;
class SimpleThreadFactory implements ThreadFactory {
    
    
    private static int count = 0 ; // 进行一个计数的操作
    public Thread newThread(Runnable r) {
    
    
        return new Thread(r, "线程 - " + count++);
    }
}
public class TestDemo {
    
    
    public static void main(String[] args) throws Exception {
    
    
        Thread t = new SimpleThreadFactory().newThread(()->{
    
    
            for (int x = 0 ; x < 10 ; x ++) {
    
    
                System.out.println(Thread.currentThread().getName()  + "、x = " + x);
            }
        }) ;
        t.start();
    }
}

以上输出的结果:

线程 - 0、x = 0
线程 - 0、x = 1
线程 - 0、x = 2
线程 - 0、x = 3
线程 - 0、x = 4
线程 - 0、x = 5
线程 - 0、x = 6
线程 - 0、x = 7
线程 - 0、x = 8
线程 - 0、x = 9

实际上对于接口对象一直强调:必须通过工厂类来获得实例化对象,所以ThreadFactory设计虽然看起来多余,但是符合标准化的设计,可是很多的开发者未必会这样去操作,在以后进行一些程序的内部结构分析上还是需要ThreadFactory。

猜你喜欢

转载自blog.csdn.net/kongfanyu/article/details/109838350