异常
异常发生的原因通常包含以下几大类:
-
用户输入了非法数据。
-
要打开的文件不存在。
-
网络通信时连接中断,或者JVM内存溢出。
三种类型的异常:
检查性异常:最具代表的检查性异常是用户错误或问题引起的异常,这是程序员无法预见的。例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略。
运行时异常: 运行时异常是可能被程序员避免的异常。与检查性异常相反,运行时异常可以在编译时被忽略。
错误: 错误不是异常,而是脱离程序员控制的问题。错误在代码中通常被忽略。例如,当栈溢出时,一个错误就发生了,它们在编译也检查不到的。
Exception 类的层次
所有的异常类是从 java.lang.Exception
类继承的子类。
Exception
类是 Throwable
类的子类。除了Exception
类外,Throwable
还有一个子类Error
。Error
用来指示运行时环境发生的错误。
Java 程序通常不捕获错误。错误一般发生在严重故障时,它们在Java程序处理的范畴之外。例如,JVM 内存溢出。一般程序不会从错误中恢复。
异常类有两个主要的子类:IOException
类和 RuntimeException
类。
内置异常类
Java 定义了一些异常类在 java.lang 标准包中,标准运行时异常类的子类是最常见的异常类。由于 java.lang
包是默认加载到所有的 Java 程序的,所以大部分从运行时异常类继承而来的异常都可以直接使用。
非检查性异常
异常 | 描述 |
---|---|
ArithmeticException | 当出现异常的运算条件时,抛出此异常。例如,一个整数"除以零"时,抛出此类的一个实例。 |
ArrayIndexOutOfBoundsException | 用非法索引访问数组时抛出的异常。如果索引为负或大于等于数组大小,则该索引为非法索引。 |
ArrayStoreException | 试图将错误类型的对象存储到一个对象数组时抛出的异常。 |
ClassCastException | 当试图将对象强制转换为不是实例的子类时,抛出该异常。 |
IllegalArgumentException | 抛出的异常表明向方法传递了一个不合法或不正确的参数。 |
IllegalMonitorStateException | 抛出的异常表明某一线程已经试图等待对象的监视器,或者试图通知其他正在等待对象的监视器而本身没有指定监视器的线程。 |
IllegalStateException | 在非法或不适当的时间调用方法时产生的信号。换句话说,即 Java 环境或 Java 应用程序没有处于请求操作所要求的适当状态下。 |
IllegalThreadStateException | 线程没有处于请求操作所要求的适当状态时抛出的异常。 |
IndexOutOfBoundsException | 指示某排序索引(例如对数组、字符串或向量的排序)超出范围时抛出。 |
NegativeArraySizeException | 如果应用程序试图创建大小为负的数组,则抛出该异常。 |
NullPointerException | 当应用程序试图在需要对象的地方使用 null 时,抛出该异常 |
NumberFormatException | 当应用程序试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式时,抛出该异常。 |
SecurityException | 由安全管理器抛出的异常,指示存在安全侵犯。 |
StringIndexOutOfBoundsException | 此异常由 String 方法抛出,指示索引或者为负,或者超出字符串的大小。 |
UnsupportedOperationException | 当不支持请求的操作时,抛出该异常。 |
java.lang 包中的检查性异常类
异常 | 描述 |
---|---|
ClassNotFoundException | 应用程序试图加载类时,找不到相应的类,抛出该异常。 |
CloneNotSupportedException | 当调用 Object 类中的 clone 方法克隆对象,但该对象的类无法实现 Cloneable 接口时,抛出该异常。 |
IllegalAccessException | 拒绝访问一个类的时候,抛出该异常。 |
InstantiationException | 当试图使用 Class 类中的 newInstance 方法创建一个类的实例,而指定的类对象因为是一个接口或是一个抽象类而无法实例化时,抛出该异常。 |
InterruptedException | 一个线程被另一个线程中断,抛出该异常。 |
NoSuchFieldException | 请求的变量不存在 |
NoSuchMethodException | 请求的方法不存在 |
异常方法
Throwable
类的主要方法
-
public String getMessage()
返回关于发生的异常的详细信息。这个消息在
Throwable
类的构造函数中初始化了。 -
public Throwable getCause()
返回一个
Throwable
对象代表异常原因。 -
public String toString()
返回此
Throwable
的简短描述。 -
public void printStackTrace()
将此
Throwable
及其回溯打印到标准错误流。 -
public StackTraceElement[] getStackTrace()
返回一个包含堆栈层次的数组。下标为0的元素代表栈顶,最后一个元素代表方法调用堆栈的栈底。
-
public Throwable fillInStackTrace()
用当前的调用栈层次填充Throwable 对象栈层次,添加到栈层次任何先前信息中。
捕获异常
使用 try
和 catch
关键字可以捕获异常。try/catch
代码块放在异常可能发生的地方。
try/catch
代码块中的代码称为保护代码,Catch
语句包含要捕获异常类型的声明。当保护代码块中发生一个异常时,try
后面的 catch
块就会被检查。
如果发生的异常包含在 catch
块中,异常会被传递到该 catch
块,这和传递一个参数到方法是一样。
finally
关键字用来创建在 try 代码块后面执行的代码块。无论是否发生异常,finally
代码块中的代码总会被执行。在 finally
代码块中,一般只是释放资源,不存放业务代码,可以运行清理类型等收尾善后性质的语句。
try
{
// 会发生异常的程序代码
}catch(ExceptionName e1)
{
//Catch 块
}
public class ExceptionTest {
public static void main(String args[]) {
try {
int a[] = new int[2];
System.out.println("Access element three :" + a[3]);
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Exception thrown:" + e);
}
System.out.println("Out of the block");
}
}
/*
* 运行结果:
* Exception thrown:java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 2
* Out of the block
*/
一个 try
代码块后面跟随多个 catch
代码块的情况就叫多重捕获。
try{
// 程序代码
}catch(ExceptionType1 异常的变量名1){ // 如果保护代码中发生异常,异常被抛给第一个 catch 块,如果抛出异常的数据类型与 ExceptionType1 匹配,它在这里就会被捕获。
// 程序代码
}catch(ExceptionType2 异常的变量名2){ // 如果不匹配,它会被传递给第二个 catch 块
// 程序代码
}catch(ExceptionType3 异常的变量名3){
// 程序代码
}
try {
file = new FileInputStream(fileName);
x = (byte) file.read();
} catch(FileNotFoundException f) { // Not valid!
f.printStackTrace();
return -1;
} catch(IOException i) {
i.printStackTrace();
return -1;
}
注意:
catch
不能独立于try
存在。- 在
try/catch
后面添加finally
块并非强制性要求的。 try
代码后不能既没catch
块也没finally
块。try
,catch
,finally
块之间不能添加任何代码。
抛出异常
如果一个方法没有捕获到一个检查性异常,那么该方法必须使用 throws
关键字来声明。throws
关键字放在方法签名的尾部,也可以使用 throw
关键字抛出一个异常,无论它是新实例化的还是刚捕获到的。
一个方法可以声明抛出多个异常,多个异常之间用逗号隔开。
public class className
{
public void withdraw(double amount) throws RemoteException,InsufficientFundsException
{
// 程序代码
}
}
声明自定义异常
编写自定义的异常类时需要注意:
-
所有异常都必须是
Throwable
的子类。 -
如果想写一个检查性异常类,则需要继承
Exception
类。 -
如果想写一个运行时异常类,那么需要继承
RuntimeException
类。
// 异常类
public class RegisterException extends Exception {
private static final long serialVersionUID = 4725328331829178789L;
public RegisterException() {
super();
}
public RegisterException(String message) {
super(message);
}
}
// 判断异常的方法类
public class RegisterDemo {
public static boolean checkName(String str) throws RegisterException {
String[] names = { "张三", "李四" };
for (String name : names) {
if (str.equals(name)) {
// 发生异常
throw new RegisterException("用户重复");
}
}
return true;
}
}
// 测试类
public class Test {
public static void main(String[] args) {
boolean isCheckName = true;
try {
isCheckName = RegisterDemo.checkName("张三");
} catch (RegisterException e) {
e.printStackTrace();
}
System.out.println(isCheckName);
}
}
进程与线程
现代操作系统(Windows,macOS,Linux)都可以执行多任务。多任务就是同时运行多个任务,CPU执行代码都是一条一条顺序执行的,但也可以同时运行多个任务。因为操作系统执行多任务实际上就是让CPU对多个任务轮流交替执行,把一个任务称为一个进程(浏览器就是一个进程,视频播放器是另一个进程),某些进程内部还需要同时执行多个子任务。例如,我们在使用Word时,Word可以一边打字,一边进行拼写检查,同时还可以在后台进行打印,我们把子任务称为线程。
进程和线程的关系:一个进程可以包含一个或多个线程,但至少会有一个线程。
多任务既可以由多进程实现,也可以由单进程内的多线程实现,还可以混合多进程+多线程。具体采用哪种方式,要考虑到进程和线程的特点。
多进程的缺点:
- 创建进程比创建线程开销大,尤其是在Windows系统上。
- 进程间通信比线程间通信慢,因为线程间通信就是读写同一个变量,速度很快。
多进程的优点:
多进程稳定性比多线程高,因为在多进程的情况下,一个进程崩溃不会影响其他进程,而在多线程的情况下,任何一个线程崩溃会直接导致整个进程崩溃。
线程
线程的状态:
-
新建状态:
使用
new
关键字和Thread
类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序start()
这个线程。 -
就绪状态:
当线程对象调用了
start()
方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。 -
运行状态:
如果就绪状态的线程获取 CPU 资源,就可以执行
run()
,此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。 -
阻塞状态:
如果一个线程执行了
sleep
(睡眠)、suspend
(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:- 等待阻塞:运行状态中的线程执行
wait()
方法,使线程进入到等待阻塞状态。 - 同步阻塞:线程在获取
synchronized
同步锁失败(因为同步锁被其他线程占用)。 - 其他阻塞:通过调用线程的
sleep()
或join()
发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join()
等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
- 等待阻塞:运行状态中的线程执行
-
死亡状态:
一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。
线程的优先级:
每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。Java 线程的优先级是一个整数,其取值范围是 1 (Thread.MIN_PRIORITY) - 10 (Thread.MAX_PRIORITY)
默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)
。
具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。
多线程编程
Java语言内置了多线程支持:一个Java程序实际上是一个JVM进程,JVM进程用一个主线程来执行main()
方法,在main()
方法内部,又可以启动多个线程。此外,JVM还有负责垃圾回收的其他工作线程等。多线程需要互相配合,复杂度高,调试更困难。
Java多线程编程的特点:
- 多线程模型是Java程序最基本的并发模型;
- 后续读写网络、数据库、Web开发等都依赖Java多线程模型。
Java 提供了三种创建线程的方法:
-
通过实现
Runnable
接口; -
通过继承
Thread
类本身; -
通过
Callable
和Future
创建线程。
通过实现 Runnable 接口
创建一个线程,最简单的方法是创建一个的类。为了实现 Runnable
,一个类只需要执行一个方法调用 run()
,public void run()
在创建一个实现 Runnable
接口的类之后,可以在类中实例化一个线程对象。Thread
定义了几个构造方法:Thread(Runnable threadOb,String threadName)
其中 threadOb
是一个实现 Runnable
接口的类的实例,并且 threadName
指定新线程的名字。
新线程创建之后,调用其 start()
方法它才会运行。
class RunnableDemo implements Runnable {
private Thread t;
private String threadName;
// 构造方法
RunnableDemo(String name) {
threadName = name;
System.out.println("Creating " + threadName );
}
// 重写run()方法
public void run() {
System.out.println("Running " + threadName );
try {
for(int i = 4; i > 0; i--) {
System.out.println("Thread: " + threadName + ", " + i);
// 让线程睡眠一会
Thread.sleep(50);
}
}catch (InterruptedException e) {
System.out.println("Thread " + threadName + " interrupted.");
}
System.out.println("Thread " + threadName + " exiting.");
}
// 重写start()方法
public void start() {
System.out.println("Starting " + threadName );
if (t == null) {
t = new Thread(this, threadName);
t.start();
}
}
}
public class TestThread {
public static void main(String args[]) {
RunnableDemo R1 = new RunnableDemo("Thread-1");
R1.start();
RunnableDemo R2 = new RunnableDemo("Thread-2");
R2.start();
}
}
/*
* 运行结果:
* Creating Thread-1
* Starting Thread-1
* Creating Thread-2
* Starting Thread-2
* Running Thread-1
* Thread: Thread-1, 4
* Running Thread-2
* Thread: Thread-2, 4
* Thread: Thread-1, 3
* Thread: Thread-2, 3
* Thread: Thread-1, 2
* Thread: Thread-2, 2
* Thread: Thread-1, 1
* Thread: Thread-2, 1
* Thread Thread-1 exiting.
* Thread Thread-2 exiting.
*/
通过继承 Thread 创建线程
创建一个新的类,该类继承 Thread 类,然后创建一个该类的实例。继承类必须重写 run()
方法,该方法是新线程的入口点,也是业务方法,启动线程后,会自动执行该方法(即调用 start()
方法,实现 Runnable
接口同理)。
注:该方法尽管被列为一种多线程实现方式,但是本质上也是实现了 Runnable
接口的一个实例。
class ThreadDemo extends Thread {
private Thread t;
private String threadName;
ThreadDemo( String name) {
threadName = name;
System.out.println("Creating " + threadName );
}
public void run() {
System.out.println("Running " + threadName );
try {
for(int i = 4; i > 0; i--) {
System.out.println("Thread: " + threadName + ", " + i);
// 让线程睡眠一会
Thread.sleep(50);
}
}catch (InterruptedException e) {
System.out.println("Thread " + threadName + " interrupted.");
}
System.out.println("Thread " + threadName + " exiting.");
}
public void start () {
System.out.println("Starting " + threadName );
if (t == null) {
t = new Thread (this, threadName);
t.start ();
}
}
}
public class TestThread {
public static void main(String args[]) {
ThreadDemo T1 = new ThreadDemo( "Thread-1");
T1.start();
ThreadDemo T2 = new ThreadDemo( "Thread-2");
T2.start();
}
}
/*
* 运行结果:
* Creating Thread-1
* Starting Thread-1
* Creating Thread-2
* Starting Thread-2
* Running Thread-1
* Thread: Thread-1, 4
* Running Thread-2
* Thread: Thread-2, 4
* Thread: Thread-1, 3
* Thread: Thread-2, 3
* Thread: Thread-1, 2
* Thread: Thread-2, 2
* Thread: Thread-1, 1
* Thread: Thread-2, 1
* Thread Thread-1 exiting.
* Thread Thread-2 exiting.
*/
线程的注意事项:
- 只有start方法才是启动线程,run方法只是在执行方法。
- 同一个线程对象,start方法不能多次调用。
- 以后都用实现Runnable接口的方式,因为Java中单继承多实现,实现的扩展性强
Thread
的静态方法:
-
public static void yield()
暂停当前正在执行的线程对象,并执行其他线程。 -
public static void sleep(long millisec)
在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。 -
public static boolean holdsLock(Object x)
当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。 -
public static Thread currentThread()
返回对当前正在执行的线程对象的引用。在哪个线程中,就返回当前线程,例如在main
方法中,则返回的参数则为main
方法当前的参数,如Thread[main,5,main]
。 -
public static void dumpStack()
将当前线程的堆栈跟踪打印至标准错误流。
Thread
的实例方法:
-
public void start()
使该线程开始执行,Java虚拟机调用该线程的
run
方法。 -
public void run()
如果该线程是使用独立的
Runnable
运行对象构造的,则调用该Runnable
对象的 run 方法;否则,该方法不执行任何操作并返回。 -
public final void setName(String name)
改变线程名称,使之与参数 name 相同。
-
public final void setPriority(int priority)
更改线程的优先级,越大优先级越高,执行越早,越小就执行越晚。
-
public final void setDaemon(boolean on)
将该线程标记为守护线程或用户线程,守护线程的默说状态和创建它的环境线程状态一致,活跃线程不可设置为守护线程(即需要在启动线程之前)。
GC就是一个守护线程,记录日志交件、消息推送、邮件分发、定时任务的线程也是守护线程。
-
public final void join(long millisec)
等待该线程终止的时间最长为 millis 毫秒。
-
public void interrupt()
中断线程。
-
public final boolean isAlive()
测试线程是否处于活动状态。
// 通过实现 Runnable 接口创建线程
public class DisplayMessage implements Runnable {
private String message;
public DisplayMessage(String message) {
this.message = message;
}
public void run() {
while(true) {
System.out.println(message);
}
}
}
// 通过继承 Thread 类创建线程
public class GuessANumber extends Thread {
private int number;
public GuessANumber(int number) {
this.number = number;
}
public void run() {
int counter = 0;
int guess = 0;
do {
guess = (int) (Math.random() * 100 + 1);
System.out.println(this.getName() + " guesses " + guess);
counter++;
} while(guess != number);
System.out.println("** Correct!" + this.getName() + "in" + counter + "guesses.**");
}
}
// 测试类
public class ThreadClassDemo {
public static void main(String [] args) {
// 创建一个Runnable对象
Runnable hello = new DisplayMessage("Hello");
// 创建一个Thread对象
Thread thread1 = new Thread(hello);
// 标记为守护线程或用户线程
thread1.setDaemon(true);
// 设置线程名字
thread1.setName("hello");
System.out.println("Starting hello thread...");
// 启动thread1线程
thread1.start();
// 创建一个Runnable对象
Runnable bye = new DisplayMessage("Goodbye");
// 创建一个Thread对象
Thread thread2 = new Thread(bye);
// 更改优先级为低优先级
thread2.setPriority(Thread.MIN_PRIORITY);
// 标记为守护线程或用户线程
thread2.setDaemon(true);
System.out.println("Starting goodbye thread...");
// 启动thread2线程
thread2.start();
System.out.println("Starting thread3...");
// 创建一个Thread对象
Thread thread3 = new GuessANumber(27);
// 启动thread3线程
thread3.start();
// 捕获异常
try {
thread3.join();
}catch(InterruptedException e) {
System.out.println("Thread interrupted.");
}
System.out.println("Starting thread4...");
// 创建一个Thread对象
Thread thread4 = new GuessANumber(75);
// 启动thread3线程
thread4.start();
System.out.println("main() is ending...");
}
}
线程同步方式
线程安全问题,当几个线程同时操作共享的数据的时候,如果同时操作这个数据就会出现数据不一致的问题,因此需要线程同步。
线程同步的方式一共有七种:
-
synchronized
修饰的方法java的每个对象都有一个内置锁,当用
synchronized
关键字修饰方法时, 内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。**线程在执行同步方法时是具有排它性的。**当任意一个线程进入到一个对象的任意一个同步方法时,这个对象的所有同步方法都被锁定了,在此期间,其他任何线程都不能访问这个对象的任意一个同步方法,直到这个线程执行完它所调用的同步方法并从中退出,从而导致它释放了该对象的同步锁之后。在一个对象被某个线程锁定之后,其他线程是可以访问这个对象的所有非同步方法的。
注:
synchronized
关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类。 -
synchronized
修饰的代码块被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。
同步块是通过锁定一个指定的对象,来对同步块中包含的代码进行同步;而同步方法是对这个方法块里的代码进行同步,而这种情况下锁定的对象就是同步方法所属的主体对象自身。如果这个方法是静态同步方法呢?那么线程锁定的就不是这个类的对象了,也不是这个类自身,而是这个类对应的java.lang.Class类型的对象。同步方法和同步块之间的相互制约只限于同一个对象之间,所以静态同步方法只受它所属类的其它静态同步方法的制约,而跟这个类的实例(对象)没有关系。
注:同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
public class SynchronizedThread { class Bank { private int account = 100; public int getAccount() { return account; } // 用同步方法实现 public synchronized void save(int money) { account += money; } // 用同步代码块实现 public void save1(int money) { synchronized (this) { account += money; } } } class NewThread implements Runnable { private Bank bank; public NewThread(Bank bank) { this.bank = bank; } @Override public void run() { for (int i = 0; i < 10; i++) { // bank.save1(10); bank.save(10); System.out.println(i + "账户余额为:" + bank.getAccount()); } } } // 建立线程,调用内部类 public void useThread() { Bank bank = new Bank(); NewThread new_thread = new NewThread(bank); System.out.println("线程1"); Thread thread1 = new Thread(new_thread); thread1.start(); System.out.println("线程2"); Thread thread2 = new Thread(new_thread); thread2.start(); } public static void main(String[] args) { SynchronizedThread st = new SynchronizedThread(); st.useThread(); } }
-
使用重入锁实现线程同步
在JavaSE5.0中新增了一个
java.util.concurrent
包来支持同步。ReentrantLock
类是可重入、互斥、实现了Lock
接口的锁,它与使用synchronized
方法和块具有相同的基本行为和语义,并且扩展了其能力。ReenreantLock
类的常用方法:-
ReentrantLock()
构造方法创建一个ReentrantLock
实例 -
lock()
: 获得锁 -
unlock()
: 释放锁
注:
ReentrantLock()
还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用。关于
Lock
对象和synchronized
关键字的选择:-
最好两个都不用,使用一种java.util.concurrent包提供的机制,能够帮助用户处理所有与锁相关的代码。
-
如果synchronized关键字能满足用户的需求,就用synchronized,因为它能简化代码
-
如果需要更高级的功能,就用ReentrantLock类,此时要注意及时释放锁,否则会出现死锁,通常在finally代码释放锁
-
-
使用特殊域变量
volatile
实现线程同步volatile
关键字为域变量的访问提供了一种免锁机制,- 使用
volatile
修饰域相当于告诉虚拟机该域可能会被其他线程更新, - 因此每次使用该域就要重新计算,而不是使用寄存器中的值
volatile
不会提供任何原子操作,它也不能用来修饰final
类型的变量
class Bank { //需要同步的变量加上volatile private volatile int account = 100; public int getAccount() { return account; } //这里不再需要synchronized public void save(int money) { account += money; } }
-
使用局部变量实现线程同步
如果使用
ThreadLocal
管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。ThreadLocal
类的常用方法:ThreadLocal()
:构造方法,创建一个线程本地变量get()
:返回此线程局部变量的当前线程副本中的值initialValue()
:返回此线程局部变量的当前线程的“初始值”set(T value)
:将此线程局部变量的当前线程副本中的值设置为value
ThreadLocal
与同步机制:ThreadLocal
与同步机制都是为了解决多线程中相同变量的访问冲突问题。- 前者采用以“空间换时间”的方法,后者采用以“时间换空间”的方式。
public class Bank { // 使用ThreadLocal类管理共享变量account private static ThreadLocal<Integer> account = new ThreadLocal<Integer>() { @Override protected Integer initialValue() { return 100; } }; public void save(int money) { account.set(account.get() + money); } public int getAccount() { return account.get(); } }
-
使用阻塞队列实现线程同步
前面5种同步方式都是在底层实现的线程同步,但是在实际开发当中,应当尽量远离底层结构。
使用
java.util.concurrent
包将有助于简化开发。 使用LinkedBlockingQueue<E>
来实现线程的同步。LinkedBlockingQueue<E>
是一个基于已连接节点的,范围任意的blocking queue
。 队列是先进先出的顺序(FIFO)。LinkedBlockingQueue
类常用方法 :LinkedBlockingQueue()
:构造方法,创建一个容量为Integer.MAX_VALUE
的LinkedBlockingQueue
put(E e)
:在队尾添加一个元素,如果队列满则阻塞size()
:返回队列中的元素个数take()
:移除并返回队头元素,如果队列空则阻塞
/* * 用阻塞队列实现线程同步 LinkedBlockingQueue的使用 */ public class BlockingSynchronizedThread { /* * 定义一个阻塞队列用来存储生产出来的商品 */ private LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<Integer>(); /* * 定义生产商品个数 */ private static final int size = 10; /* * 定义启动线程的标志,为0时,启动生产商品的线程;为1时,启动消费商品的线程 */ private int flag = 0; private class LinkBlockThread implements Runnable { @Override public void run() { int new_flag = flag++; System.out.println("启动线程 " + new_flag); if (new_flag == 0) { for (int i = 0; i < size; i++) { int b = new Random().nextInt(255); System.out.println("生产商品:" + b + "号"); try { queue.put(b); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("仓库中还有商品:" + queue.size() + "个"); try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } else { for (int i = 0; i < size / 2; i++) { try { int n = queue.take(); System.out.println("消费者买去了" + n + "号商品"); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("仓库中还有商品:" + queue.size() + "个"); try { Thread.sleep(100); } catch (Exception e) { // TODO: handle exception } } } } } public static void main(String[] args) { BlockingSynchronizedThread bst = new BlockingSynchronizedThread(); LinkBlockThread lbt = bst.new LinkBlockThread(); Thread thread1 = new Thread(lbt); Thread thread2 = new Thread(lbt); thread1.start(); thread2.start(); } }
注:
BlockingQueue<E>
定义了阻塞队列的常用方法,尤其是三种添加元素的方法,我们要多加注意,当队列满时:-
add()
方法会抛出异常 -
offer()
方法返回false
-
put()
方法会阻塞
-
使用原子变量实现线程同步
需要使用线程同步的根本原因在于对普通变量的操作不是原子的。
原子操作就是指将读取变量值、修改变量值、保存变量值看成一个整体来操作,即这几种行为要么同时完成,要么都不完成。
在java的
util.concurrent.atomic
包中提供了创建了原子类型变量的工具类,使用该类可以简化线程同步。其中
AtomicInteger
表可以用原子方式更新int
的值,可用在应用程序中(如以原子方式增加的计数器),但不能用于替换Integer
,可扩展Number
,允许那些处理机遇数字类的工具和实用工具进行统一访问。原子操作主要有:
- 对于引用变量和大多数原始变量(
long
和double
除外)的读写操作; - 对于所有使用
volatile
修饰的变量(包括long
和double
)的读写操作。
AtomicInteger
类常用方法:AtomicInteger(int initialValue)
:构造方法创建具有给定初始值的新的AtomicInteger
addAddGet(int dalta)
:以原子方式将给定值与当前值相加get()
:获取当前值
class Bank { private AtomicInteger account = new AtomicInteger(100); public AtomicInteger getAccount() { return account; } public void save(int money) { account.addAndGet(money); } }
- 对于引用变量和大多数原始变量(
悲观锁与乐观锁
-
悲观锁:
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。
传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中
synchronized
和ReentrantLock
等独占锁就是悲观锁思想的实现。 -
乐观锁:
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于
write_condition
机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic
包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。 -
使用场景:
两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。