Java 线程学习笔记(二)—— 进阶篇

在Java 5.0 提供了java.util.concurrent(简称JUC )包,在此包中增加了在并发编程中很常用的实用工具类,用于定义类似于线程的自定义子系统,包括线程池、异步IO 和轻量级任务框架。提供可调的、灵活的线程池。还提供了设计用于多线程上下文中的Collection 实现等。

volatile 关键字 内存可见性

package cn.fg.test03;

public class Tester {

	public static void main(String[] args) {
		Hello hello = new Hello();
		new Thread(hello).start();
		while (true) {
			if (hello.isFlag()) {
				System.out.println("----------------------------");
				break;
			}
		}
	}
	 
}

class Hello implements Runnable {
	
	private boolean flag = false;

	@Override
	public void run() {
		
		try {
			Thread.sleep(1000);   //模拟逻辑处理睡1秒
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		flag = true; //业务逻辑处理完置标志
		System.out.println(flag);
	}

	public boolean isFlag() {
		return flag;
	}
}

通过执行上述代码,你会发现while循环永远不会break,main线程会一直等待。为什么?hello线程花了1秒来处理逻辑,然后设置的flag的值,main线程进入while时flag为false,由于while底层效率非常高,main线程永远读不到true,如果你在while外或里面也睡一会,就会读到flag=true了。

如何解决?加上synchronized代码块即可

	public static void main(String[] args) {
		Hello hello = new Hello();
		new Thread(hello).start();
		
		while (true) {
			synchronized (hello) {
				if (hello.isFlag()) {
					System.out.println("----------------------------");
					break;
				}
			}
			
		}
	}

虽然synchronized可以解决,但是不是最优选择。Java提供了一种稍弱的同步机制,即volatile 变量,用来确保将变量的更新操作通知到其他线程。可以将volatile 看做一个轻量级的锁,但是又与锁有些不同:对于多线程,不是一种互斥关系;不能保证变量状态的“原子性操作”。说简单点,volatile 只能解决内存可见,不能解决线程安全。

//volatile 解决内存可见问题,加上volatile后,会实时更新主存,没有了缓存,效率要稍差些
//加与不加,具体看业务场景
private volatile boolean flag = false;

Atomic 原子变量

还是先来看一段代码

package cn.fg.test03;

public class TestAtomicDemo {

	public static void main(String[] args) {
		AtomicDemo ad = new AtomicDemo();
		for (int i = 0; i < 10; i++) {
			new Thread(ad).start();
		}
	}
}

class AtomicDemo implements Runnable{
	
	private int serialNumber = 0;
	
	@Override
	public void run() {
		serialNumber++;  //自加1
		System.out.println(serialNumber);
	}
}

在做++操作的时候,会有线程安全问题,可以使用synchronized来解决;这里我们使用另外一种解决方式,使用AtomicXXX原子变量。为什么叫原子变量?我们以++语法为例,++操作实际是做了以下几步:

1. int temp = serialNumber ; 
2. serialNumber = serialNumber + 1; 
3. serialNumber = temp;

可以简单理解为事务,读、写、改必须一步完成,不能被其他操作修改其值;原子变量底层使用了CAS算法

所以在java.util.concurrent.atomic包下为我们提供了原子类,这里我们使用AtomicInteger来修改代码

package cn.fg.test03;

import java.util.concurrent.atomic.AtomicInteger;

public class TestAtomicDemo {

	public static void main(String[] args) {
		AtomicDemo ad = new AtomicDemo();
		for (int i = 0; i < 10; i++) {
			new Thread(ad).start();
		}
	}
}

class AtomicDemo implements Runnable{
	
	private AtomicInteger serialNumber = new AtomicInteger(0);
	
	@Override
	public void run() {
		System.out.println(serialNumber.getAndIncrement());
	}
}

使用原子变量的效率比synchronized高,synchronized可以看成是悲观锁,Atomic可以看成是乐观锁。

CopyOnWriteArrayList 写入并复制

写入并复制,该list解决的是arraylist在add()的时候线程安全的问题,那么arraylist到底有什么线程安全问题呢?

package cn.fg.test04;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Tester {

	public static void main(String[] args) {
		Hello hello = new Hello();
		for (int i = 0; i < 100 ; i++) {
			new Thread(hello).start();
		}
	}
	 
}

class Hello implements Runnable {

	//private List<String> list = Collections.synchronizedList(new ArrayList<String>());
	private ArrayList<String> list = new ArrayList<String>();

	@Override
	public void run() {

		try {
			Thread.sleep(10);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		//此处会发生数组越界,下标的值被其他线程覆盖的问题
		//更多线程安全问题 https://blog.csdn.net/u010416101/article/details/88720974
		list.add(Thread.currentThread().getName());

	}
}
	//使用CopyOnWriteArrayList(写入并复制)替换ArrayList
	//虽然CopyOnWriteArrayList是线程安全的,但是频繁add效率很低,因为每次add的时候都会copy一个新的list进行add
	CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();

CountDownLatch 闭锁

我们现在有这样一个场景,在主线程中开启5个线程分别去做不同的事,然后统计5个线程的总用时。

package cn.fg.juc1;

import java.util.concurrent.CountDownLatch;

public class TestCountDownLatch {

	public static void main(String[] args) {
		
		CountDownLatch latch = new CountDownLatch(2);  //构造参数是计数器
		
		long start = System.currentTimeMillis();

		new Thread(new DoOne(latch)).start();  //做第一件事
		new Thread(new DoTow(latch)).start();  //做第二件事

		try {
			latch.await();  //等待阻塞中,计数器为0时才会执行后面的代码
		} catch (InterruptedException e) {
		}

		long end = System.currentTimeMillis();

		System.out.println("耗费时间为:" + (end - start));
	}

}

//做第一件事
class DoOne implements Runnable {

	private CountDownLatch latch;

	public DoOne(CountDownLatch latch) {
		this.latch = latch;
	}

	@Override
	public void run() {
		try {
			System.out.println("DoOne.....................done"); //模拟做完了
		} finally {
			//做完这件事后,要调用一下countDown(),计数器才会-1,因此要保证该条语句一定要被执行到,否则main线程就会一直阻塞
			//该方法是线程安全的
			latch.countDown(); 
		}

	}

}

//做第二件事
class DoTow implements Runnable {

	private CountDownLatch latch;

	public DoTow(CountDownLatch latch) {
		this.latch = latch;
	}

	@Override
	public void run() {
		try {
			Thread.sleep(5000);
			System.out.println("DoTow.....................done"); //模拟做完了
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			latch.countDown();
		}

	}

}

另外callabel接口的线程也能实现该功能 

//class NumThread implements Callable<Integer>
NumThread numThread = new NumThread();
FutureTask<Integer> futureTask = new FutureTask<Integer>(numThread); //callable实现类需要与
new Thread(futureTask).start();
Integer sum = futureTask.get();

Lock锁的使用

java.util.concurrent.locks.Lock 接口是 控制多个线程对共享资源进行访问的工具。 锁提供了对共享资源的独占访问,每次只能有一个线程对 Lock 对象加锁,线程开始访问共享资源之前应先获得 Lock 对象

1. ReentrantLock

ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义, 在 实现线程安全的控制中,比较常用的是 ReentrantLock 可以显式加锁、释放锁 。

package cn.fg.test02;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * lock锁的线程通信例子:使用两个线程打印 1-100。线程1, 线程2 交替打印
 *
 */
class Number implements Runnable {
	private int number = 1;
	private Lock lock = new ReentrantLock();
	private Condition condition = lock.newCondition(); //等同于synchronized的监视器对象

	@Override
	public void run() {

		while (true) {

			try {
				lock.lock();
				
				condition.signal();  //等同于this.notify()
				//condition.signalAll(); //等同于this.notifyAll()

				if (number <= 100) {
					System.out.println(Thread.currentThread().getName() + ":" + number);
					number++;

					try {
						condition.await();  //等同于this.wait()
					} catch (InterruptedException e) {
						e.printStackTrace();
					}

				} else {
					break;
				}
			} finally {
				lock.unlock();
			}

		}

	}
}

public class CommunicationTest {
	public static void main(String[] args) {
		Number number = new Number();
		Thread t1 = new Thread(number);
		Thread t2 = new Thread(number);

		t1.setName("线程1");
		t2.setName("线程2");

		t1.start();
		t2.start();
	}
}

2. Condition 线程通讯

Condition 接口描述了可能会与锁有关联的条件变量。这些变量在用法上与使用Object.wait 访问的隐式监视器类似,但提供了更强大的功能。需要特别指出的是,单个Lock 可能与多个Condition 对象关联

package cn.fg.test02;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * lock锁的线程通信例子:使用两个线程打印 1-100。线程1, 线程2 交替打印
 *
 */
class Number implements Runnable {
	private int number = 1;
	private Lock lock = new ReentrantLock();
	private Condition condition = lock.newCondition(); //等同于synchronized的监视器对象

	@Override
	public void run() {

		while (true) {

			try {
				lock.lock();
				
				condition.signal();  //等同于notify()
				//condition.signalAll(); //等同于notifyAll()

				if (number <= 100) {
					System.out.println(Thread.currentThread().getName() + ":" + number);
					number++;

					try {
						condition.await();  //等同于wait()
					} catch (InterruptedException e) {
						e.printStackTrace();
					}

				} else {
					break;
				}
			} finally {
				lock.unlock();
			}

		}

	}
}

public class CommunicationTest {
	public static void main(String[] args) {
		Number number = new Number();
		Thread t1 = new Thread(number);
		Thread t2 = new Thread(number);

		t1.setName("线程1");
		t2.setName("线程2");

		t1.start();
		t2.start();
	}
}
package cn.fg.test02;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/*
 * 编写一个程序,开启 3个线程ABC,实现轮流打印 ,例如:ABCABCABCABC
 */
public class TestABCAlternate {
	
	public static void main(String[] args) {
		AlternateDemo ad = new AlternateDemo();
	
		new Thread(new Runnable() {
			@Override
			public void run() {
				
				for (int i = 1; i <= 20; i++) {
					ad.printA();
				}
				
			}
		}, "A").start();
		
		
		new Thread(new Runnable() {
			@Override
			public void run() {
				
				for (int i = 1; i <= 20; i++) {
					ad.printB();
				}
				
			}
		}, "B").start();
		
		new Thread(new Runnable() {
			@Override
			public void run() {
				
				for (int i = 1; i <= 20; i++) {
					ad.printC();
					
					System.out.println("-----------------------------------");
				}
				
			}
		}, "C").start();
	}

}

class AlternateDemo{
	
	private int number = 1; //当前正在执行线程的标记
	
	private Lock lock = new ReentrantLock();
	private Condition condition1 = lock.newCondition();
	private Condition condition2 = lock.newCondition();
	private Condition condition3 = lock.newCondition();
	

	public void printA(){
		lock.lock();
		
		try {
			//1. 判断
			if(number != 1){
				condition1.await(); //A线程等待
			}
			
			//2. 打印
			System.out.print(Thread.currentThread().getName());
			
			//3. 唤醒B线程
			number = 2;
			condition2.signal();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}
	
	public void printB(){
		lock.lock();
		
		try {
			//1. 判断
			if(number != 2){
				condition2.await(); //B线程等待
			}
			
			//2. 打印
			System.out.print(Thread.currentThread().getName());
			
			//3. 唤醒C线程
			number = 3;
			condition3.signal();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}
	
	public void printC(){
		lock.lock();
		
		try {
			//1. 判断
			if(number != 3){
				condition3.await(); //C线程等待
			}
			
			//2. 打印
			System.out.print(Thread.currentThread().getName());
			
			//3. 唤醒A线程
			number = 1;
			condition1.signal();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}
	
}

ReadWriteLock 读写锁

读写锁:分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由jvm自己控制的,我们只要上好相应的锁即可。如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁;如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。总之,读的时候上读锁,写的时候上写锁!
读写锁接口:ReadWriteLock,它的具体实现类为:ReentrantReadWriteLock

package cn.fg.test04;

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/*
 * 1. ReadWriteLock : 读写锁
 * 
 * 写写/读写 需要“互斥”
 * 读读 不需要互斥
 */
public class TestReadWriteLock {

	public static void main(String[] args) {
		ReadWriteLockDemo rw = new ReadWriteLockDemo();
		
		//一个线程用来写数据
		new Thread(new Runnable() {
			
			@Override
			public void run() {
				rw.set((int)(Math.random() * 101));
			}
		}, "Write:").start();
		
		
		//两个线程用来读取数据
		for (int i = 0; i < 2; i++) {
			new Thread(new Runnable() {
				
				@Override
				public void run() {
					rw.get();
				}
			}).start();
		}
	}
	
}

class ReadWriteLockDemo{
	
	private int number = 0;
	
	private ReadWriteLock lock = new ReentrantReadWriteLock();
	
	//读
	public void get(){
		//上读锁,上锁后,若其他线程为写锁,根据“写写/读写”互斥原则,其他线程需要等待该读线程执行完毕后才可以执行;若其他线程为读锁或无锁,则不会发生任何影响
		lock.readLock().lock(); 
		
		try{
			System.out.println(Thread.currentThread().getName() + " : " + number);
		}finally{
			lock.readLock().unlock(); //释放读锁
		}
	}
	
	//写
	public void set(int number){
		System.out.println("准备写入数据.....................");
		
		lock.writeLock().lock(); //上写锁
		
		//为了演示读写锁,这里我们将写数据睡1秒。3个线程同时执行,若先执行的是写线程,则两个读线程将会等待,待写线程释放锁后,根据读读不互斥原则,两个读线程不会发生等待,会同时执行
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		try{
			System.out.println("写入完毕");
			this.number = number;
		}finally{
			lock.writeLock().unlock(); //释放写锁
		}
	}
}

https://www.cnblogs.com/liang1101/p/6475555.html?utm_source=itdadao&utm_medium=referral

线程八锁

下面示例是线程锁的实践,多个线程执行不同的带synchronized 的方法时,并不一定是同时执行的,关键是搞清楚执行线程中持有的锁是否是同一把锁,若是同一把锁,当其中一个锁被锁住时,其他线程则进入阻塞

package cn.fg.test04;

/*
 * 题目:判断打印的 "one" or "two" ?
 * 
 * 1. 两个普通同步方法,两个线程,标准打印, 打印? //one  two
 * 2. 新增 Thread.sleep() 给 getOne() ,打印? //one  two
 * 3. 新增普通方法 getThree() , 打印? //three  one   two
 * 4. 两个普通同步方法,两个 Number 对象,打印?  //two  one
 * 5. 修改 getOne() 为静态同步方法,打印?  //two   one
 * 6. 修改两个方法均为静态同步方法,一个 Number 对象?  //one   two
 * 7. 一个静态同步方法,一个非静态同步方法,两个 Number 对象?  //two  one
 * 8. 两个静态同步方法,两个 Number 对象?   //one  two
 * 
 * 线程八锁的关键:
 * ①非静态方法的锁默认为  this,  静态方法的锁为 对应的 Class 实例
 * ②某一个时刻内,只能有一个线程持有锁,无论几个方法。
 */
public class TestThread8Monitor {
	
	public static void main(String[] args) {
		Number number = new Number();
		Number number2 = new Number();
		
		new Thread(new Runnable() {
			@Override
			public void run() {
				number.getOne();
			} 
		}).start();
		
		new Thread(new Runnable() {
			@Override
			public void run() {
//				number.getTwo();
				number2.getTwo();
			}
		}).start();
		
		/*new Thread(new Runnable() {
			@Override
			public void run() {
				number.getThree();
			}
		}).start();*/
		
	}

}

class Number{
	
	public static synchronized void getOne(){//Number.class
		try {
			Thread.sleep(3000);
		} catch (InterruptedException e) {
		}
		
		System.out.println("one");
	}
	
	public synchronized void getTwo(){//this
		System.out.println("two");
	}
	
	public void getThree(){
		System.out.println("three");
	}
	
}

newScheduledThreadPool 调度线程池

package cn.fg.test04;

import java.util.Random;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/*
 * 一、线程池:提供了一个线程队列,队列中保存着所有等待状态的线程。避免了创建与销毁额外开销,提高了响应的速度。
 * 
 * 二、线程池的体系结构:
 * 	java.util.concurrent.Executor : 负责线程的使用与调度的根接口
 * 		|--**ExecutorService 子接口: 线程池的主要接口
 * 			|--ThreadPoolExecutor 线程池的实现类
 * 			|--ScheduledExecutorService 子接口:负责线程的调度
 * 				|--ScheduledThreadPoolExecutor :继承 ThreadPoolExecutor, 实现 ScheduledExecutorService
 * 
 * 三、工具类 : Executors 
 * ExecutorService newFixedThreadPool() : 创建固定大小的线程池
 * ExecutorService newCachedThreadPool() : 缓存线程池,线程池的数量不固定,可以根据需求自动的更改数量。
 * ExecutorService newSingleThreadExecutor() : 创建单个线程池。线程池中只有一个线程
 * 
 * ScheduledExecutorService newScheduledThreadPool() : 创建固定大小的线程,可以延迟或定时的执行任务。
 */
public class TestScheduledThreadPool {

	public static void main(String[] args) throws Exception {
		ScheduledExecutorService pool = Executors.newScheduledThreadPool(5);
		for (int i = 0; i < 5; i++) {
			 pool.schedule(new Runnable() {
				
				@Override
				public void run() {
					int num = new Random().nextInt(100);//生成随机数
					System.out.println(Thread.currentThread().getName() + " : " + num);
				}
			}, 1, TimeUnit.SECONDS);  //延迟1秒钟执行,TimeUnit.SECONDS 可以更改延迟的单位,秒、小时、天等
			
		}
		
		pool.shutdown();
	}
	
}

更多调度线程用法 https://blog.csdn.net/weixin_39792935/article/details/105357000

Fork/Join 框架

Fork/Join 框架:就是在必要的情况下,将一个大任务,进行拆分(fork)成若干个小任务(拆到不可再拆时),再将一个个的小任务运算的结果进行join 汇总。

 

Fork/Join 框架与线程池的区别
采用“工作窃取”模式(work-stealing):
当执行新的任务时它可以将其拆分分成更小的任务执行,并将小任务加到线程队列中,然后再从一个随机线程的队列中偷一个并把它放在自己的队列中。
相对于一般的线程池实现,fork/join框架的优势体现在对其中包含的任务的处理方式上.在一般的线程池中,如果一个线程正在执行的任务由于某些原因无法继续运行,那么该线程会处于等待状态。而在fork/join框架实现中,如果某个子问题由于等待另外一个子问题的完成而无法继续运行。那么处理该子问题的线程会主动寻找其他尚未运行的子问题来执行.这种方式减少了线程的等待时间,提高了性能。

 

package cn.fg.juc2;

import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;
import java.util.stream.LongStream;

import org.junit.Test;

//从0累加到500亿,拆分若干任务的的示例
public class TestForkJoinPool {
	
	public static void main(String[] args) {
		Instant start = Instant.now();
		
		ForkJoinPool pool = new ForkJoinPool();
		
		ForkJoinTask<Long> task = new ForkJoinSumCalculate(0L, 50000000000L);
		
		Long sum = pool.invoke(task);
		
		System.out.println(sum);
		
		Instant end = Instant.now();
		
		System.out.println("耗费时间为:" + Duration.between(start, end).toMillis());//166-1996-10590
	}
	
	@Test
	public void test1(){
		Instant start = Instant.now();
		
		long sum = 0L;
		
		for (long i = 0L; i <= 50000000000L; i++) {
			sum += i;
		}
		
		System.out.println(sum);
		
		Instant end = Instant.now();
		
		System.out.println("耗费时间为:" + Duration.between(start, end).toMillis());//35-3142-15704
	}
	
	//java8 新特性
	@Test
	public void test2(){
		Instant start = Instant.now();
		
		Long sum = LongStream.rangeClosed(0L, 50000000000L)
							 .parallel()
							 .reduce(0L, Long::sum);
		
		System.out.println(sum);
		
		Instant end = Instant.now();
		
		System.out.println("耗费时间为:" + Duration.between(start, end).toMillis());//1536-8118
	}

}

class ForkJoinSumCalculate extends RecursiveTask<Long>{

	/**
	 * 
	 */
	private static final long serialVersionUID = -259195479995561737L;
	
	private long start;
	private long end;
	
	private static final long THURSHOLD = 10000L;  //临界值
	
	public ForkJoinSumCalculate(long start, long end) {
		this.start = start;
		this.end = end;
	}

	@Override
	protected Long compute() {
		long length = end - start;
		
		if(length <= THURSHOLD){
			long sum = 0L;
			
			for (long i = start; i <= end; i++) {
				sum += i;
			}
			
			return sum;
		}else{
			long middle = (start + end) / 2;
			
			ForkJoinSumCalculate left = new ForkJoinSumCalculate(start, middle); 
			left.fork(); //进行拆分,同时压入线程队列
			
			ForkJoinSumCalculate right = new ForkJoinSumCalculate(middle+1, end);
			right.fork(); //
			
			return left.join() + right.join();
		}
	}
	
}

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值