Java基础知识:并发(上)

  • 操作系统将CPU的时间片分配给每一个进程,给人并行处理的感觉。
  • 多进程和多线程的本质区别:每个进程都拥有自己的一整套变量,而线程共享数据(通信更有效方便同时有风险)。
  • 有些操作系统中,创建和撤销线程开销相对进程而言要得多。

 

  • 单线程实现弹跳小球,示例程序:
    • 若程序试图用repaint方法更新图形的绘制,则会在addBall方法返回之后才会重新绘制画板。
    • BallComponent扩展于JPanel,使得擦除背景变得很容易(JPanel本身不带有任何组件,就是一块空板子,调用super.paintComponent(g)就是画一个空板子,就是变相的背景擦除)。
    • 这个程序的性能和用户体验不好(每次弹跳结束才能UI交互)。
package bounce;

import javax.swing.JFrame;

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.event.*;
import javax.swing.*;
public class BounceFrame extends JFrame {
	public static void main(String[] args) {
		EventQueue.invokeLater(()->{
			BounceFrame frame = new BounceFrame();
			frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
			frame.setVisible(true);			
		});
	}
	
	
	private BallComponent comp;
	private static final int STEP = 1000;
	private static final int DELAY = 5;
	
	public BounceFrame() {
		comp = new BallComponent();
		JPanel buttonPanel = new JPanel();
		add(comp, BorderLayout.CENTER);
		add(buttonPanel, BorderLayout.SOUTH);
		
		JButton startButton = new JButton("Start");
		JButton closeButton = new JButton("Close");
		buttonPanel.add(startButton);
		buttonPanel.add(closeButton);
		
		startButton.addActionListener(event->addBall());
		closeButton.addActionListener(event->System.exit(0));
		pack();
	}
	
	public void addBall() {
		Ball ball = new Ball();
		comp.add(ball);
		try {
			for( int i = 0; i < STEP; i++ ) {
				ball.move(comp.getBounds());
				comp.paint(comp.getGraphics());
				Thread.sleep(DELAY);			
			}

		}catch(InterruptedException e ) {
			
		}
	}
}
package bounce;

import java.awt.geom.*;
public class Ball {
	private static final int X_SIZE = 15;
	private static final int Y_SIZE = 15;
	private double x = 0;
	private double y = 0;
	private double dx = 1;
	private double dy = 1;
	
	public void move(Rectangle2D bounds) {
		x += dx;
		y += dy;
		
		if( x < bounds.getMinX() ) {
			x = bounds.getMinX();
			dx = -dx;
		}
		if( x + X_SIZE > bounds.getMaxX() ) {
			x = bounds.getMaxX() - X_SIZE;
			dx = -dx;
		}
		
		if( y < bounds.getMinY() ) {
			y = bounds.getMinX();
			dy = -dy;
		}
		
		if( y + Y_SIZE > bounds.getMaxY() ) {
			y = bounds.getMaxY() - Y_SIZE;
			dy = -dy;
		}
		
	}
	
	public Ellipse2D getShape() {
		return new Ellipse2D.Double(x, y, X_SIZE, Y_SIZE);
	}
}
package bounce;

import javax.swing.JPanel;

import java.util.*;
import java.awt.*;
public class BallComponent extends JPanel {
	private static final int DEFAULT_WIDTH = 450;
	private static final int DEFAULT_HEIGHT = 350;
	
	private java.util.List<Ball> balls = new ArrayList<>();
	
	public void add(Ball b) {
		balls.add(b);
	}
	
	public void paintComponent(Graphics g) {
		super.paintComponent(g);
		
		Graphics2D g2 = (Graphics2D) g;
		for( Ball b : balls ) {
			g2.fill(b.getShape());
		}
	}
	
	public Dimension getPreferredSize() { return new Dimension(DEFAULT_WIDTH, DEFAULT_HEIGHT); }
}

 

  •  使用线程给其他任务提供机会:
    • 可以将移动球的代码放置在一个独立的线程中。实际上,可以发起多个球,每个球都在自己的线程中运行。另外,AWT的 事件分派线程 将一直地并行运行(处理界面事件)。
    • 如果需要执行一个比较耗时的任务,应当并发地运行任务。
    • 在一个单独的线程中执行一个任务的简单过程:
      • 将任务代码移到实现了 Runnable接口 的类 的 run方法中(可以使用lambda表达式)。
      • 由Runnable创建一个Thread对象。Thread t = new Thread(r)
      • 启动线程。t.start() 
    • 警告:不要调用Thread类或Runnable对象的run方法,直接调用run方法,只会执行同一个线程中的任务,而不会启动新线程。
    • 示例程序:
package bouceThread;

import java.awt.BorderLayout;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
public class BounceFrame extends JFrame{
	public static void main(String[] args) {
		
		EventQueue.invokeLater(()->{
			BounceFrame frame = new BounceFrame();
			frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
			frame.setVisible(true);			
		});
	}
	
	private BallComponent comp;
	private static final int STEP = 1000;
	private static final int DELAY = 5;
	
	public BounceFrame() {
		comp = new BallComponent();
		add(comp, BorderLayout.CENTER);
		
		JPanel buttonPanel = new JPanel();
		makeButton(buttonPanel, "Start", event->addBall());
		makeButton(buttonPanel, "Close", event->System.exit(0));
		add(buttonPanel, BorderLayout.SOUTH);
		
		pack();
	}
	
	public void makeButton( Container c, String name, ActionListener listener) {
		JButton button = new JButton(name);
		c.add(button);
		button.addActionListener(listener);
	}
	
	public void addBall() {
		Ball ball = Ball.getRandomBall(comp.getBounds());
		comp.add(ball);
		
		Thread ballThread = new Thread(()->{
			try {
				
				for( int i = 0; i < STEP; i++ ) {
					ball.move(comp.getBounds());
					comp.repaint();
					Thread.sleep(DELAY);
				}
			}catch(InterruptedException e ) {
				
			}
		});
		ballThread.start();
	}
}
package bouceThread;
import java.awt.geom.*;
import java.util.*;
public class Ball {
	private static final int X_SIZE = 15;
	private static final int Y_SIZE = 15;
	
	private double x = 0;
	private double y = 0;
	private double dx = 1;
	private double dy = 1;
	
	public Ball() {}
	
	private Ball(double x, double y) {
		this.x = x;
		this.y = y;
	}
	
	public void move( Rectangle2D bounds ) {
		x += dx;
		y += dy;
		
		if( x < bounds.getMinX() ) {
			x = bounds.getMinX();
			dx = -dx;
		}
		
		if( x + X_SIZE >= bounds.getMaxX() ) {
			x = bounds.getMaxX() - X_SIZE;
			dx = -dx;
		}
		
		if( y < bounds.getMinY() ) {
			y = bounds.getMinY();
			dy = -dy;
		}
		if( y + Y_SIZE >= bounds.getMaxY() ) {
			y = bounds.getMaxY() - Y_SIZE;
			dy = -dy;
		}
	}
	
	public Ellipse2D getShape() {
		return new Ellipse2D.Double(x, y, X_SIZE, Y_SIZE);
	}
	
	public static Ball getRandomBall(Rectangle2D bounds) {
		double minX = bounds.getMinX();
		double maxX = bounds.getMaxX() - X_SIZE;
		double minY = bounds.getMinY();
		double maxY = bounds.getMaxX() - Y_SIZE;
		
		Random generator = new Random();
		double rndX = generator.nextDouble();
		double rndY = generator.nextDouble();
		
		rndX = rndX*(maxX - minX) + minX;
		rndY = rndY*(maxY - minY) + minY;
		return new Ball(rndX, rndY);
	}
	
}
package bouceThread;

import javax.swing.JPanel;

import java.util.*;
import java.util.List;
import java.awt.*;
public class BallComponent extends JPanel {
	private static final int DEFAULT_WIDTH = 450;
	private static final int DEFAULT_HEIGHT = 350;
	
	private List<Ball> balls = new ArrayList<>();
	
	public void paintComponent(Graphics g) {
		super.paintComponent(g);
		
		Graphics2D g2 = (Graphics2D) g;
		g2.setPaint(Color.PINK);
		for( Ball b : balls ) {
			g2.fill(b.getShape());
		}
	} 
	
	public void add(Ball ball) {
		balls.add(ball);
	}
	
	public Dimension getPreferredSize() {
		return new Dimension(DEFAULT_WIDTH, DEFAULT_HEIGHT);
	}
}

 

  • 中断线程:
    •  当线程的run方法执行方法体中最后一条语句后,并经由执行return语句返回时,或者出现了在方法中没有捕获的异常,线程将终止
    • interrupt方法可以用来请求终止线程(线程的 中断状态 被置位)。
    • 调用 静态的 Thread.currentThread方法 可以获得当前的线程
    • 当在一个被阻塞的线程(调用sleep或wait)上调用interrupt方法时,阻塞调用 将会InterruptedException异常 中断
    • 普遍的情况是,线程简单地将中断作为一个终止的请求。当然,被中断的线程可以决定如何响应中断。
    • 警告:如果在中断状态被置位时调用sleep方法,它不会休眠。相反,它将清除这一状态抛出InterruptedException异常
    • java.lang.Thread
      • void interrupt()
      • static boolean interrupted() //检测当前线程是否被中断,会把当前的线程的中断状态重置
      • boolean isInterrupted()
      • static Thread currentThread()
  • 线程状态:
    • 线程可以有6种状态:
      • New 新创建
      • Runnable 可运行
      • Blocked 被阻塞
      • Waiting 等待
      • Timed waiting 计时等待
      • Terminated 被终止
    • 新创建线程:程序还没开始运行线程中的代码。在线程运行之前还有一些基础工作要做。
    • 运行线程:一旦调用了start方法,线程处于Runnable状态。一个可运行状态的线程可能在运行,也可能不在运行。
      • 事实上,运行中的线程被中断目的是为了让其他线程获得运行机会线程调度的细节依赖于操作系统提供的服务。
      • 抢占式调度:系统给每一个可运行线程一个 时间片 来执行任务,时间片用完,操作系统剥夺该线程的运行权。(桌面以及服务器操作系统)
      • 协作式调度:一个线程只有在调用 yield方法被阻塞等待时,线程才失去控制权。(手机等小型设备)
      • 多个处理器的机器,每个处理器可以运行一个线程,可以有多个线程并行运行。
    • 被阻塞线程和等待线程:不运行任何代码且消耗最少的资源。直到 线程调度器 重新激活它。
      • 线程试图获取一个内部的对象锁,而该锁被其他线程持有,则该线程进入阻塞状态。当所有其他线程释放该锁并且线程调度器允许本线程持有它的时候,则该线程变成非阻塞状态。
      • 当线程等待另一个线程通知调度器一个条件时,它自己进入等待状态。阻塞状态和等待状态大不相同
      • 有几个方法有一个超时参数,调用它们线程进入 计时等待状态。
    • 被终止的线程:两种原因
      • 因为run方法正常退出而自然死亡
      • 因为一个没有被捕获的异常终止了run方法而意外死亡
      • stop方法已经过时,不要使用。stop方法抛出ThreadDead错误对象,由此杀死线程。
    • java.lang.Thread
      • void join()  //等待终止指定的线程
      • void join()  //等待指定的线程死亡或经过指定的毫秒数
      • Thread.State getState()  //获取线程的状态
      • void stop()  //停止该线程。这一方法已过时
      • void suspend()  //暂停线程的执行。这一方法已过时
      • void resume() //恢复线程。与suspend相配合。这一方法已过时 
  • 线程属性
    • 线程优先级:在Java中,每一个线程有一个优先级。默认情况下,一个线程继承它的父线程的优先级。优先级可以通过setPriority方法 设为1到10之间的任何值。MIN_PRIORITY  1 、NORM_PRIORITY 5 、MAX_PRIORITY 10
      • 线程的优先级高度依赖于系统。优先级会被映射到宿主机平台的优先级上。
      • 避免过度使用线程优先级:如果高优先级的线程没有进入非活动状态,低优先级的线程可能 永远也不能执行
      • java.lang.Thread
        • void setPriority( int newPriority)
        • static void yield()  //导致当前线程处于让步状态。如果有其他的可运行线程具有至少与此线程同样高的优先级,那么这些线程接下来会被调度。
    • 守护线程:
      • 守护线程的唯一用途时为其他线程提供服务
      • 只剩下守护线程时,虚拟机就退出了
      • 守护线程应该永远不去访问固有资源,如文件、数据库,因为它们会在任何时候甚至在一个操作的中间发生中断。(需要考虑 关机动作
      • java.lang.Thread
        • void setDaemon(boolean b) //标识该线程为守护线程或用户线程。必须在线程启动前调用
    • 未捕获异常处理器
      •  非受查异常会导致线程终止。就在线程死亡之前,异常被传递到一个用于 未捕获异常的处理器。该处理器必须属于一个实现了 Thread.UncaughtExceptionHandler接口 的类。
        • void uncaughtException(Thread t, Throwable e)
      • 可以用 setUncaughtExceptionHandler方法为任何线程安装一个处理器。也可以用静态方法setDefaultUncaughtExceptionHandler为所有线程安装一个默认的处理器。
      • 如果不安装默认的处理器,默认的处理器为空。但是,如果不为独立的线程安装处理器,此时的处理器就是 该线程的 ThreadGroup对象。线程组是一个可以统一管理线程集合
        • 如果该线程组有父线程组,父线程组的uncaughtException方法被调用
        • 否则,如果Thread.getDefaultUncaughtHandler返回一个非空处理器,则调用处理器。
        • 否则,如果Throwable对象是 ThreadDead 的一个实例,什么也不做
        • 否则,线程名以及栈轨迹输出到 System.err 上。
  • 同步
    • 在大多实际的多线程应用中,两个或以上的线程需要共享同一数据的存取。根据各线程访问数据的次序,可能会产生讹误的对象,这样的情况通常称为 竞争条件(race condition)
    • 锁对象
      • 有两种机制防止代码块受并发访问的干扰
        • Java提供一个 synchronized关键字 达到这一目的。
        • Java SE 5.0 引入了 ReentrantLock类
//用 ReentrantLock保护代码块的基本结构如下:
myLock.lock();
try{
    critical section;  //关键代码 
}
finally{
    myLock.unLock(); //make sure the lock is unlocked even if an exception is thrown
}
  • 这一结构 确保 任何时刻只有一个线程进入临界区。
  • 一旦一个线程封锁了锁对象,其他任何线程都无法通过 lock语句。当其他线程调用lock时,它们被阻塞直到第一个线程释放锁对象
  • finally子句的解锁操作至关重要,否则当临界区的代码抛出异常,但锁没解锁的话,其他线程将会永远阻塞
  • 如果两个线程试图访问同一个Bank对象,那么锁以串行的方式提供服务。如果两个线程访问不同的Bank对象,那么两个线程都不会阻塞。
  • 锁是可重入的,线程可以重复地获得已经持有的锁。锁保持一个 持有计数跟踪对lock方法的嵌套调用
  • 如果使用锁,就不能使用带资源的try语句
  • java.util.concurrent.locks.ReentrantLock
    • ReentrantLock()
    • ReentrantLock(boolean fair) //公平锁策略,偏爱等待时间最长的线程。但会大大降低性能

 

  • 条件对象
    • 通常,线程进入临界区,却发现在某一条件满足之后它才能执行。要使用一个 条件对象 来管理那些已经获得了一个锁但是却不能做有用工作的线程。
    • 一个锁对象可以有一个或多个相关的条件对象。
    • Condition sufficientFunds = new bankLock.newCondition()
    • 如果发现条件不满足,调用 sufficientFunds.await()。当前线程被阻塞了并放弃了锁
    • 一旦一个线程调用await方法,它进入该条件的等待集。当锁可用时,该线程不能马上解除阻塞,相反,它处于阻塞状态,直到另一个线程调用同一条件上的signalAll方法为止。signalAll不会立刻激活一个等待线程,仅仅是解除等待线程的阻塞
    • 当一个线程调用 await方法时,它没有办法重新激活自身,它寄希望于其他线程。如果没有其他线程重新激活等待的线程,它就永远不再运行了,这将导致令人不快的 死锁(deadlock)现象。
    • signal方法,随机解除等待集中的某个线程的阻塞状态。有风险。万一随机选择的等待线程仍然不能运行,会再次阻塞,但如果没有其他线程再次调用signal,那么系统就 死锁 了。
  • 实例程序:(模拟银行转账)
package syncBank;

public class SyncBankTest {
	private static final int ACCOUNT_N = 100;
	private static final double ACCOUNT_INI = 1000;
	private static final double MAX_AMOUNT = 1000;
	public static final int DELAY = 10;
	
	public static void main(String[] args) {
		Bank bank = new Bank(ACCOUNT_N, ACCOUNT_INI);
		
		for( int i = 0; i < ACCOUNT_N; i++ ) {
			int from = i;
			Runnable r = ()->{
				try {
					while(true) {
						int to = (int) ( bank.getSize() * Math.random() );
						double amount = MAX_AMOUNT * Math.random();
						bank.tranfer(from, to, amount);
						Thread.sleep((int)( DELAY * Math.random() ));
					}
				}catch(InterruptedException e) {
					
				}
			};
			Thread t = new Thread(r);
			t.start();
		}
	}
}
package syncBank;

import java.util.*;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class Bank {
	private ReentrantLock bankLock = new ReentrantLock();
	private Condition sufficientFunds = bankLock.newCondition();
	
	private final double[] accounts;
	
	public Bank( int account_num, double per ) {
		accounts = new double[account_num];
		Arrays.fill(accounts, per);
	}
	
	
	public void tranfer( int from, int to, double amount ) throws InterruptedException {
		bankLock.lock();
		try {
			while( accounts[from] < amount ) {
				sufficientFunds.await();
			}
			accounts[from] -= amount;
			accounts[to] += amount;
			System.out.print(Thread.currentThread());
			System.out.printf("  %10.2f from %d to %d", amount, from, to );
			System.out.printf("  Total Balance: %10.2f%n", getTotalBalance());
			//if( to == from ) System.out.println("---");
			sufficientFunds.signalAll();
		}finally {
			bankLock.unlock();
		}
	}
	
	public double getTotalBalance() {
		
		bankLock.lock();
		try {
			double sum = 0;
			for( double account : accounts ) {
				sum += account;
			}
			return sum;			
		}finally {
			bankLock.unlock();
		}

	}
	
	public int getSize() {
		return accounts.length;
	}
}

 

  • synchronized关键字
    • Lock和Condition接口 为程序设计人员提供了 高度的锁定控制。然而,大多数情况下,并不需要那样的控制。
    • java中的每个对象都有一个内部锁。如果用 synchronized关键字 声明一个方法,那么对象的锁将保护整个方法。也就是说,要调用该方法,线程必须获得内部的对象锁。(内部锁)
    • 内部对象锁只有一个相关条件wait方法添加到一个线程等待集中,notifyAll/notify方法解除等待线程的阻塞状态。
    • 内部锁和条件的局限性
      • 不能中断一个正在试图获得锁的线程
      • 试图获得锁时不能设定超时
      • 每个锁仅有单一的条件,可能不够
    • 建议:
      • 最好既不使用 Lock/Condition,也不使用 synchronized关键字。许多情况下,可以使用 java.util.concurrent包中的一种机制,它会为你处理所有的加锁。
      • 如果 synchronized关键字适合你的程序,就尽量使用它。
      • 如果特别需要 Lock/Condition结构 的独有特性,才使用~。
    • java.lang.Object
      • void notifyAll()  //只能在同步方法或者同步块内部调用。如果当前线程不是对象锁的持有者,抛出异常。
      • void notify()
      • void wait()
      • void wait(long millis)  //导致线程进入等待状态直到被通知或者经过指定的时间
      • void wait(long millis, int nanos)  //millis毫秒,nanos纳秒
    • 示例程序:(synchronized关键字)
package sync2;

public class SynchronizedTest {
	private static final int ACCOUNT_N = 100;
	private static final double MAX_AMOUNT = 1000;
	private static final double ACCOUNT_INI = 1000;
	private static final int DELAY = 10;
	
	public static void main(String[] args) {
		Bank bank = new Bank(ACCOUNT_N, ACCOUNT_INI);
		
		for( int i = 0; i < ACCOUNT_N; i++ ) {
			int from = i;
			Runnable r =()->{
				try {
					while(true) {
						int to = (int) (bank.getSize()*Math.random());
						double amount = MAX_AMOUNT * Math.random();
						bank.transer(from, to, amount);
						Thread.sleep((int)(DELAY*Math.random()));
					}
				}catch(InterruptedException e) {
					
				}	
			};
			Thread t = new Thread(r);
			t.start();
		}
	}
}
package sync2;

import java.util.Arrays;

public class Bank {
	private double[] accounts;
	
	public Bank( int accounts_num, double initBalance ) {
		accounts = new double[accounts_num];
		Arrays.fill(accounts, initBalance);
	}
	
	public synchronized void transer(int from, int to, double amount) throws InterruptedException {
		if( accounts[from] < amount )
			wait();
		
		accounts[from] -= amount;
		accounts[to] += amount;
		System.out.print(Thread.currentThread());
		System.out.printf("  %10.2f from %d to %d", amount, from, to );
		System.out.printf("  Total Balance: %10.2f%n", getTotalBalance() );
		notifyAll();
	}
	
	public synchronized double getTotalBalance() {
		double sum = 0;
		for( double a : accounts ) {
			sum += a;
		}
		return sum;
	}
	
	public int getSize() { return accounts.length; }
}

 

  • 同步阻塞
    • 每个Java对象有一个锁。线程可以通过调用同步方法获得锁。还有另一种机制可以获得锁,通过进入一个同步阻塞
synchronized(obj){
    critical section;
}
//获得obj的锁
  • 有时程序员使用一个对象的锁来实现额外的原子操作,实际上称为 客户端锁定。依赖于类对自己的所有可修改方法都使用内部锁。所以客户端锁定是非常脆弱的,通常不推荐使用

 

  • 监视器概念
    • 不需要程序员考虑如何加锁的情况下,就可以保证多线程的安全性。最成功的解决方案之一就是 监视器(monitor)
    • 监视器的特性:
      • 监视器是只包含私有域的类
      • 每个监视器类的对象有一个相关的锁
      • 使用该锁对所有的方法进行加锁
      • 该锁可以有任意多个相关条件
    • Java设计者以不是很精确的方式采用了监视器概念,使得线程的安全性下降,Java对象与监视器的区别:
      • 域不要求必须是private
      • 方法不要求必须是synchronized
      • 内部锁对客户是可用的
  • Volatile 域
    • 有时,如果仅仅为了读写一个或两个实例域就使用同步,显得花销过大
    • volatile关键字为实例域的同步访问提供了一种免锁机制。如果声明一个域为volatile,那么编译器和虚拟机就直到该域是可能被另一个线程并发更新的。
    • volatile变量不能提供原子性
  • 原子性
    • java.util.concurrent.atomic包中有很多类使用了很高效的机器级指令来保证其他操作(除了赋值之外)的原子性。
    • AtomicInteger类的 incrementAndGet 和 decrementAndGet,将一个整数自增 和 自减。
    • compareAndSet方法,如果另一个线程也在更新同一个对象,不设置新值返回false(可利用do-while循环完成更新)。
    • updateAndGet方法,提供一个lambda表达式(它会为你不断重试)完成更新。
      • largest.updateAndGet(x->Math.max(x,observed))
    • accumulateAndGet方法利用一个 二元操作符 来合并原子值和所提供的参数(循环重试)。
      • largest.accumulateAndGet(observed, Math::max)
    • 如果认为可能存在大量竞争,只需要使用 LongAdder 而不是 AtomicLong 。
    • LongAccumulator。
  • 死锁
    • 所有的线程都被阻塞
    • Java没有任何东西可以避免或打破死锁,因此,必须仔细设计程序,以确保不会出现死锁。
  • 线程局部变量
    • java.lang.ThreadLocal<T>
      • T get()  //得到这个线程的当前值。如果是首次调用,则调用initialize初始化
      • protected initialize()
      • void set(T t)
      • void remove()
      • static <S> ThreadLocal<S> withInitial( Supplier<? extends S> supplier )
  • 锁测试与超时
    • tryLock方法试图申请一个锁,成功获得锁后返回true,否则返回false。(直接调用lock方法很可能发生阻塞)
    • 可以调用tryLock时,使用超时参数
    • 如果调用带有超时参数的tryLock,那么线程在等待期间被中断,将抛出InterruptedException异常
    • java.util.concurrent.locks.Lock
      • boolean tryLock()  //尝试获得锁而不发生阻塞。如果成功返回真。这个方法会抢夺可用的锁。
    • java.util.concurrent.locks.Condition
      • boolean await( long time, TimeUnit unit )  //进入该条件的等待集,直到线程从等待集移出或者超时时间到才解除阻塞。如果等待时间到了而返回就返回false,否则返回true。
  • 读/写锁 ReentrantReadWriteLock类
    • 如果很多线程从一个数据结构读取数据而很少线程修改其中的数据,此类非常有用。
    • 步骤
      • 构造一个 ReentrantReadWriteLock 对象
      • 抽取 读锁 和 写锁
      • 对所有的获取方法加 读锁
      • 对所有的修改方法加 写锁
    • java.util.concurrent.locks.ReentrantReadWriteLock
      • Lock readLock()  //读锁,排斥所有写操作
      • Lock writeLock()  //写锁,排斥所有的 读操作 和 写操作
  • 为什么弃用stop 和 suspend 方法
    • stop方法:当线程终止另一个线程时,无法知道何时调用stop方法才是安全的。(线程被终止,立即释放它锁住所有对象的锁,而如果此时正在进行某对象进行的修改操作未完成,那这个对象就被破坏了。但其他线程只知道锁被释放而不知道这个对象被破坏而继续操作)
    • suspend方法:如果 线程B suspend挂起一个持有锁的线程A,而这个线程B又试图获得同一个锁,程序就陷入死锁。(线程A被阻塞但持有锁,线程B试图得到锁,但是锁不会被线程A释放,从而陷入死锁)
    • 如果想安全地挂起线程,引入一个变量 suspendResquested 并在 run方法 的某个安全的地方测试它,安全的地方是指 该线程没有封锁其他线程需要的对象的地方

 

  • 阻塞队列
    • 许多线程问题,可以通过一个或多个队列以优雅且安全的方式将其形式化。使用队列,可以安全地从一个线程向另一个线程传递数据。在协调多个线程之间的合作时,阻塞队列是一个有用的工具。
    • 阻塞队列方法分为3类,取决于当队列满或空时它们的响应方式:
      • put/take方法,如果队列满或空,阻塞。(将队列当作线程管理工具)
      • add/remove/element方法,抛出异常
      • offer/poll/peek方法,返回false/null/null 。(如果不能完成任务,只给出错误提示而不会抛出异常)
    • java.util.concurrent包 提供了 阻塞队列 的几个变种
      • LinkedBlockingQueue
      • ArrayBlockingQueue
      • PriorityBlockingQueue
      • DelayQueue
    • 示例程序:(利用阻塞队列不需显式的线程同步实现 枚举文件 和 处理文件的 并行处理)
package blockingQueue;

import java.util.*;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.io.*;
public class BlockingQueueTest {
	private static final int QUEUE_SIZE = 10;
	private static final int SEARCH_THREADS = 100;
	private static final String DEFAULT_DIR = ".";
	private static final File DUMMY = new File("");
	private static BlockingQueue<File> queue = new ArrayBlockingQueue<>(QUEUE_SIZE);
	
	
	public static void main(String[] args) {
		try(Scanner in = new Scanner(System.in) ){
			System.out.println("Enter keyword:");
			File file = new File(DEFAULT_DIR);
			String keyword = in.next();
			
			Runnable enumerator = ()->{
				try {
					enumerate(file);
					queue.put(DUMMY);
				}catch(InterruptedException e) {
					
				}
			};
			new Thread(enumerator).start();
			
			for(int i = 1; i <= SEARCH_THREADS; i++ ) {
				Runnable searcher = ()->{
					try {
						boolean done = false;
						while(!done) {
							File f = queue.take();
							if( f == DUMMY ) {
								queue.put(f);
								done = true;
							}else search(f, keyword);
							
						}
					}catch(IOException e) {
						
					}catch(InterruptedException e) {
						
					}
				};
				new Thread(searcher).start();
			}
			
		}
	}
	
	public static void enumerate(File directory) throws InterruptedException{
		File[] files = directory.listFiles();
		for( File file : files ) {
			if( file.isDirectory() ) enumerate(file);
			else queue.put(file);
		}
	}
	
	public static void search(File file, String keyword) throws IOException{
		try(Scanner in = new Scanner(file, "UTF-8")){
			int lineNumber = 0;
			while(in.hasNextLine()) {
				lineNumber++;
				String line = in.nextLine();
				if( line.contains(keyword)) {
					System.out.printf("%s: %d %s%n", file.getPath(), lineNumber, line );
				}
			}
		}
	}
}

 

线程安全的集合

  • java.util.concurrent.ConcurrentLinkedQueue<E>  
  • java.util.concurrent.ConcurrentSkipListSet<E>
  • java.util.concurrent.ConcurrentHashMap<K,V>
  • java.util.concurrent.ConcurrentSkipListMap<K,V>
  • 集合返回弱一致性的迭代器。迭代器并不一定能反映出它们被构造之后的所有的修改,但是,它们不会将同一个值返回两次,也不会抛出 ConcurrentModificationException异常
  • ​​​​​​​映射条目的原子更新
    • get和put虽然不会破坏数据结构但是操作序列不是原子的(两个原子操作之间的间隙有机可乘),结果不可预知。如利用get和put操作进行计数更新,会存在多个线程同时在进行更新同一计数而导致结果错误。
    • 使用 ConcurrentHashMap<String,AtomicLong> 或者 Concurrent<String,LongAdder>
    • 调用compute方法,可以提供一个键和一个计算新值的函数 
      • map.compute( word, (k,v)-> v == null? 1 : v + 1)
    • merge方法,map.merge(word, 1L, Long::sum)
    • 使用compute方法和merge方法时,提供的函数不能做太多工作,否则会阻塞对映射的其他更新。
  • 对并发散列映射的批操作
    • 即使有其他线程正在处理映射,这些操作也能安全地执行。除非你恰好知道批操作运行时映射不会被修改,否则就要把结果看作是映射状态的一个近似
    • 批操作的三种类型
      • 搜索search 为每个键或值提供一个函数,知道函数生成一个非null的结果,然后搜索终止返回结果
      • 归约reduce 组合所有的键或值
      • forEach 为所有的键或值提供一个函数
    • 每个操作有4个版本
      • operationKeys 处理键
      • operationValue 处理值
      • operation 处理键和值
      • operationEntries  处理Map.Entry对象
      • 对于上述各个操作,需要指定一个 参数化阈值(parallelism threshold)。如果映射包含的元素多于这个阈值,就会并行完成批操作。如果希望批操作在一个线程中运行,使用 Long.MAX_VALUE ,如果希望尽可能多的线程运行批操作,可以使用 阈值1
    • forEach 和 reduce操作都有 提供转换器函数 的版本,转换器函数可以作为过滤器
  • 并发集视图
    • 如果想要一个大的线程安全的集而不是映射。并没有一个ConcurrentHashSet类。
    • ConcurrentHashMap的静态方法 <K>newKeySet 生成一个 Set<K> ,这实际上是 ConcurrentHashMap<K,Boolean>的包装器。(所有的值都映射成Boolean.TRUE)
    • 如果原本就有一个映射,生成一个键集。
      • <K>newKeySet()  //可删不可增
      • <K>newKeySet(defaultVal)  //可删可增 
  • 写数组的拷贝:CopyOnWriteArrayList 和 CopyOnWrite
    • 如果在集合进行迭代的线程数超过修改线程数(读多写少),这样的安排是很有用的。
    • 原理:写操作上锁,且写操作修改的是原底层数组的拷贝,完成操作再设置为新数组。读操作不上锁,直接访问的是底层数组(所以可能是过时的视图)。
  • 并行数组算法:
    • parallelSort
    • parallelSetAll
    • parallelPrefix
  • 较早的线程安全集合
    • Vector 和 HashTable类提供了线程安全的动态数组和散列表的实现。但是已经被弃用了。
    • 任何集合类都可以通过使用 同步包装器 变成线程安全的:
      • List<E> synchArrayList = Collections.synchronizedList( new ArrayList<E>())
      • Map<K,V> synchHashMap = Collections.synchronizedMap( nwe HashMap<K,V>())
    • 应该确保没有任何线程通过原始的非同步方法访问数据结构。
    • 如果在另一个线程可能进行修改时要对集合进行迭代,仍然需要使用 “客户端”锁定 。否则,在迭代过程中如果另一个线程修改集合,迭代器会失效,抛出ConcurrentModificationException异常。
    • 最好使用 java.util.concurrent包中定义的集合而不使用同步包装器中的。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值