Java线程并发控制基础知识

微博上众神推荐今年4月刚刚出版的一本书,淘宝华黎撰写的《大型网站系统与Java中间件实践》,一线工程师的作品,实践出真知,果断要看。

前两章与《淘宝技术这十年》内容类似,基本是讲从一个小网站如何慢慢升级成分布式网站,从第三章开始亮出干货,个人感觉总结的很好,本文主要摘取并扩充下作者第三章的内容

作学习交流之用,非盈利性质


线程池、线程同步、互斥锁、读写锁、原子数、唤醒、通知、信号量、线程交换队列

线程池


推荐用ThreadPoolExecutor的工厂构造类Executors来管理线程池,线程复用线程池开销较每次申请新线程小,具体看代码以及注释


public class TestThread {
/**
* 使用线程池的方式是复用线程的(推荐)
* 而不使用线程池的方式是每次都要创建线程
* Executors.newCachedThreadPool(),该方法返回的线程池是没有线程上限的,可能会导致过多的内存占用
* 建议使用Executors.newFixedThreadPool(n)
*
* 有兴趣还可以看下定时线程池:SecheduledThreadPoolExecutor
*/
public static void main(String[] args) throws InterruptedException, ExecutionException {
int nThreads = 5;

/**
* Executors是ThreadPoolExecutor的工厂构造方法
*/
ExecutorService executor = Executors.newFixedThreadPool(nThreads);

//submit有返回值,而execute没有返回值,有返回值方便Exception的处理
Future res = executor.submit(new ConsumerThread());
//executor.execute(new ConsumerThread());

/**
* shutdown调用后,不可以再submit新的task,已经submit的将继续执行
* shutdownNow试图停止当前正执行的task,并返回尚未执行的task的list
*/
executor.shutdown();

//配合shutdown使用,shutdown之后等待所有的已提交线程运行完,或者到超时。继续执行后续代码
executor.awaitTermination(1, TimeUnit.DAYS);

//打印执行结果,出错的话会抛出异常,如果是调用execute执行线程那异常会直接抛出,不好控制,submit提交线程,调用res.get()时才会抛出异常,方便控制异常
System.out.println("future result:"+res.get());
}

static class ConsumerThread implements Runnable{

@Override
public void run() {
for(int i=0;i<5;i++) {
System.out.println(i);
}
}
}
}


输出:
0
1
2
3
4
future result:null

线程同步


synchronized(this)和synchronized(MyClass.class)区别:前者与加synchronized的成员方法互斥,后者和加synchronized的静态方法互斥

synchronized的一个应用场景是单例模式的,双重检查锁


public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
  if (singleton == null) {
  singleton = new Singleton();
  }
}
}
return singleton;
}
}

注意:不过双重检查锁返回的实例可能是没有构造完全的对象,高并发的时候直接使用有问题,不知道在新版的java里是否解决了

所以有了内部类方式的单例模式,这样的单例模式有了延迟加载的功能(还有一种枚举方式的单例模式,用的不多,有兴趣的可以上网查)


//(推荐)延迟加载的单例模式
public class Singleton {
private static class SingletonHolder {
  private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
  return SingletonHolder.INSTANCE;
}
}

若不要延迟加载,在类加载的时候实例化对象,那直接这么写,如下:


public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
  return instance;
}
}

volatile保证同一变量在多线程中的可见性,所以它更多是用于修饰作为开关状态的变量

用synchronized修饰变量的get和set方法,不但可以保证和volatile修饰变量一样的效果(获取最新值),因为synchronized不仅会把当前线程修改的变量的本地副本同步给主存,还会从主存中读取数据更新本地副本。而且synchronized还有互斥的效果,可以有效控制并发修改一个值,因为synchronized保证代码块的串行执行。如果只要求获取最新值的特性,用volatile就好,因为volatile比较轻量,性能较好。

互斥锁、读写锁

ReentrantLock 和 ReentrantReadWriteLock

JDK5增加了ReentrantLock这个类因为两点:

1.ReentrantLock提供了tryLock方法,tryLock调用的时候,如果锁被其他线程(同一个线程两次调用tryLock也都返回true)持有,那么tryLock会立即返回,返回结果是false。lock()方法会阻塞。

2.构造RenntrantLock对象可以接收一个boolean类型的参数,描述锁公平与否的函数。公平锁的好处是等待锁的线程不会饿死,但是整体效率相对低一些;非公平锁的好处是整体效率相对高一些。

注意:使用ReentrantLock后,需要显式地进行unlock,所以建议在finally块中释放锁,如下:


lock.lock();
try {
//do something
}
finally {
lock.unlock();
}

ReentrantReadWriteLock与ReentrantLock的用法类似,差异是前者通过readLock()和writeLock()两个方法获得相关的读锁和写锁操作。

原子数


除了用互斥锁控制变量的并发修改之外,jdk5中还增加了原子类,通过比较并交换(硬件CAS指令)来避免线程互斥等待的开销,进而完成超轻量级的并发控制,一般用来高效的获取递增计数器。

AtomicInteger counter = new AtomicInteger();
counter.incrementAndGet();
counter.decrementAndGet();

可以简单的理解为以下代码,增加之后与原先值比较,如果发现增长不一致则循环这个过程。代码如下


public class CasCounter {
private SimulatedCAS value;
public int getValue() {
return value.getValue();
}
public int increment() {
int oldValue = value.getValue();
while (value.compareAndSwap(oldValue, oldValue + 1) != oldValue)
oldValue = value.getValue();
return oldValue + 1;
}
}

可以看IBM工程师的一篇文章Java 理论与实践: 流行的原子

唤醒、通知


wait,notify,notifyAll是java的Object对象上的三个方法,多线程中可以用这些方法完成线程间的状态通知。

notify是唤醒一个等待线程,notifyAll会唤醒所有等待线程。

CountDownLatch主要提供的机制是当多个(具体数量等于初始化CountDownLatch时的count参数的值)线程都到达了预期状态或完成预期工作时触发事件,其他线程可以等待这个事件来触发后续工作。

举个例子,大数据分拆给多个线程进行排序,比如主线程


CountDownLatch latch = new CountDownLatch(5);

for(int i=0;i<5;i++) {
threadPool.execute(new MyRunnable(latch,datas));
}

latch.await();

//do something 合并数据

MyRunnable的实现代码如下

public void run() {
//do something数据排序
latch.countDown();
   //继续自己线程的工作,与CyclicBarrier最大的不同,稍后马上讲
}

CyclicBarrier循环屏障,协同多个线程,让多个线程在这个屏障前等待,直到所有线程都到达了这个屏障时,再一起继续执行后面的动作

使用CyclicBarrier可以重写上面的排序代码

主线程如下


CyclicBarrier barrier = new CyclicBarrier(5+1); //主线程也要消耗一个await,所以+1

for(int i=0;i<5;i++) {
threadPool.execute(new MyRunnable(barrier,datas));//如果线程池线程数过少,就会发生死锁
}

barrier.await();
//合并数据

MyRunnable代码如下

public void run() {
//数据排序
barrier.await();
}

//全部 count+1 await之后(包括主线程),之后的代码才会一起执行

信号量


Semaphore用于管理信号量,与锁的最大区别是,可以通过令牌的数量,控制并发数量,当管理的信号量只有1个时,就退化到互斥锁。

例如我们需要控制远程方法的并发量,代码如下


semaphore.acquire(count);
try {
//调用远程方法
}
finally {
semaphore.release(count);
}

线程交换队列

Exchanger用于在两个线程之间进行数据交换,线程会阻塞在Exchanger的exchange方法上,直到另外一个线程也到了同一个Exchanger的exchanger方法时,二者进行交换,然后两个线程继续执行自身相关代码。


public class TestExchanger {
static Exchanger exchanger = new Exchanger();
public static void main(String[] args) {
new Thread() {
public void run() {
int a = 1;
try {
a = (int) exchanger.exchange(a);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("Thread1: "+a);
}
}.start();

new Thread() {
public void run() {
int a = 2;
try {
a = (int) exchanger.exchange(a);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("Thread2: "+a);
}
}.start();
}
}

输出结果:

Thread2: 1
Thread1: 2

并发容器


CopyOnWrite思路是在更改容器时,把容器写一份进行修改,保证正在读的线程不受影响,适合应用在读多写少的场景,因为写的时候重建一次容器。

以Concurrent开头的容器尽量保证读不加锁,并且修改时不影响读,所以会达到比使用读写锁更高的并发性能

最后向大家推荐下,淘宝华黎的这本书《大型网站系统与Java中间件实战》

时间: 2024-10-11 08:50:29

Java线程并发控制基础知识的相关文章

Java并发(基础知识)—— Executor框架及线程池

在Java并发(基础知识)—— 创建.运行以及停止一个线程中讲解了两种创建线程的方式:直接继承Thread类以及实现Runnable接口并赋给Thread,这两种创建线程的方式在线程比较少的时候是没有问题的,但是当需要创建大量线程时就会出现问题,因为这种使用方法把线程创建语句随意地散落在代码中,无法统一管理线程,我们将无法管理创建线程的数量,而过量的线程创建将直接使系统崩溃. 从高内聚角度讲,我们应该创建一个统一的创建以及运行接口,为我们管理这些线程,这个统一的创建与运行接口就是JDK 5的Ex

Java语言的基础知识9

第十一章(线程) 1.通过String name=Thread.currentThread().getName();来获取当前线程的名称. 2.多次启动一个线程或者启动一个已经运行的线程是非法的,会抛出IllegalThreadStateException异常对象. Thread.sleep((int)Math.random()*10000); 3.java提供了Runnable接口使继承了其他类之后同样可以实现该接口达到创建线程的目的Runabble接口同样定义了Run方法. 实现Runnab

java线程方面的知识

java中单继承,多实现的: 若为多继承,那么当多个父类中有重复的属性或者方法时,子类的调用结果会含糊不清,因此用了单继承. 为什么是多实现呢? 通过实现接口拓展了类的功能,若实现的多个接口中有重复的方法也没关系,因为实现类中必须重写接口中的方法,所以调用时还是调用的实现类中重写的方法.那么各个接口中重复的变量又是怎么回事呢? 接口中,所有属性都是 static final修饰的,即常量,这个什么意思呢,由于JVM的底层机制,所有static final修饰的变量都在编译时期确定了其值,若在使用

Java语言的基础知识

第三章 1.在java源文件编辑器中,选择某个成员变量,然后按住shift+alt+j,Eclipse会自动添加JavaDoc文档注释结构,如果选择的是方法,还会自动添加参数名称. 2.Java语言规定标示符是由任意的字母.下划线.美元符号和数字组成,并且第一个字符不能使数字,标示符不能使java中的保留关键字. 3.在Java语言中允许使用汉字或其他语言文字作为变量名,如int 年龄 =21;在程序运行时不会报错,但建议尽量不要使用这些语言作为变量. 4.java用关键字final来声明常量,

Java语言的基础知识4

第五章(数组) 1.在Java中可以将数组看做是一个对象虽然基本数据类型不是对象但有基本数据类型组成的数组是对象. 2.对于二维数组求第二维就用array[0].length, array.length就是默认的是第一维的长度. 3.foreach并不是一个新的语法它是for的循环的格式化主要执行遍历功能的循环,example: int arry ={1,2,3,4,5}; for(int i :array){ system.out.println(): } 4.数组元素定义完以后可通过Arra

黑马程序员——Java I/O基础知识之I/O流

I/O流基础知识--字节流和字符流 文件存储在硬盘中,是以二进制表示的,只有内存中才能形成字符.数据的来源可以有硬盘,内存,控制台,网络,Java把数据从一个地方转到另一个地方的现象称为流,用InputStream和OutputStream接口来表示,这两个流里面的都是以字节为单位的,后来加入了Reader和Writer,里面操作的是字符,是两个字节为单位的. 字节流 字节流将数据写入文件 try { File file =new File("d:" +File .separator+

Java语言的基础知识12

第十四章(使用集合类保存对象) 1.java中得集合对象就像是一个容器,它用来存放Java类的对象.Java中的集合类有些方便存入和取出,有些则方便查找.集合类和数组的区别是,数组的长度是固定的,集合的长度是可变的,数组用来存放基本类型,集合用来存放对象的引用.常用的集合类有List集合,Set集合,和Map集合. 2.List集合包括List接口以及List接口的所有实现类.List集合中的元素许重复,个元素的顺序就是对象插入的顺序.类似java中的数组.List类继承了Collection接

Java语言的基础知识10

第十二章(GUI事件) 1.GUI事件的处理机制是建立交互式应用程序的关键技术,其中事件是用在程序界面上的各种操作. 2.写程序的时候对于swing的一些空间譬如jprogressbar ,jtextfield等空间在全局中声明以后,用的时候一定要new 一下,自己经常忘记(由于对java理解不深) private  JTextField textField2; textField2 = new JTextField(); 3.事件在java语言中也是一种对象 4.监听器接受到事件之后,将委托指

Java语言的基础知识3

第四章 1.sum +=x++;的语句等价于sum=sum+x;和x=x+1;两条语句 2.do....while循环语句与while循环语句类似.他们之间的差别是while语句为先判断条件是否成立再执行循环体,而do...while循环语句是先执行一次循环体,在判断条件是否成立.也就是说do....while循环语句至少执行一次循环体. 3.在一个循环语句中又包含另外一个完整的循环语句,称为嵌套循环,笔者不建议使用超过3层的嵌套循环,因为这样会使程序更加难以阅读. 4.乘法口诀中用到的制表符"