- 一个程序同时执行多个任务,通常,每一个任务称为一个线程
- 进程与线程的本质区别在于每个进程拥有自己的一整套变量,而线程则共享数据。共享数据使线程之间的通信比进程之间的通信更有效、更容易
- 创建线程的步骤:1.构造一个实现了Runnable接口的对象(需要实现Runnable接口的run方法)。2.将该对象传入Thread的构造函数 。3.调用Thread对象的start方法
public class Test {
public static void main(String[] args){
Runnable r = new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
for(int i = 0;i < 10;i++) {
try {
System.out.println(i);
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO: handle exception
}
}
}
};//使用匿名函数构造一个实现了Runnable接口的对象,只需要实现run方法即可
Thread t = new Thread(r);
t.start();
try {
Thread.sleep(1000);
System.out.println("hello");
} catch (InterruptedException e) {
// TODO: handle exception
}
}
}
- ThreadObject.interrupt():若该线程被wait()、join()或者sleep()阻塞了,那么这个线程调用interrupt将抛出一个InterruptedException
- 线程的五种状态:
- 新建。即线程被创建
- 就绪。调用线程的start方法
- 运行。线程运行Runnable对象的run方法
- 阻塞。join:等待目标完成后转成就绪状态 sleep:等待超时后转成就绪状态 wait:等待获得信号后转成就绪状态
- 结束。run()完成或者异常退出
- 要确定一个线程的当前状态,可调用getState方法
- 新创建线程:如new Thread(r),该线程还没有开始运行,线程状态为New
- 可运行线程:一旦调用start方法,线程即处于runnable状态。一个可运行的线程可能正在运行也可能没有运行,这取决于操作系统给线程提供开始运行的时间
- 被阻塞线程:当一个线程试图获取一个内部的对象锁,而该锁被其他线程持有,则该线程进入阻塞状态。当所有其他线程释放该锁,并且线程调度器允许持有它的时候,该线程将变为非阻塞状态
- 等待线程:当线程等待另一个线程通知调度器一个条件时,它自己进入等待状态。比如在调用Object.wait方法或Thread.join方法或者等待java.util.concurrent库中的Lock或Condition时,就会出现这种情况
- 计时等待线程:调用一些有超时参数的方法将导致线程进入计时等待状态,这一状态将一直保持到超时期满或者接收到适当的通知。带有超时参数的方法有Thread.sleep、Object.wait、Thread.join、Lock.tryLock以及Condition.await的计时版
- 线程被终止的情况:
- 正常终止。run方法正常退出而自然死亡
- 意外死亡。因为一个没有捕获的异常而终止了run方法从而意外死亡
- 默认情况下,一个线程继承它的父线程的优先级
- sleep和join区别:sleep只是单纯地暂停该线程,而join会引起父进程的阻塞(可以理解为把某线程t插入到当前线程中,并阻塞当前线程),即只有当子进程运行完毕后,父进程才继续运行。代码佐证如下:
/*
输出结果为
0 1 2 3 4 hh
*/
public class Test {
public static void main(String[] args) throws InterruptedException{
Runnable r = new Runnable() {
@Override
public void run() {
for(int i = 0;i < 5;i++) {
try {
System.out.println(i);
Thread.sleep(1000);
} catch (Exception e) {
// TODO: handle exception
return;
}
}
}
};
Thread t = new Thread(r);
t.start();
t.join(5000);//使线程t阻塞main线程,设置join的超时参数为5000,是确保t线程在5s内能执行完毕
System.out.println("hh");
}
}
- 使用getPriority()和setPriority()获得当前线程的优先级和设置当前线程的优先级
- MIN_PRIORITY = 1,MAX_PRIORITY = 10
- NORM_PRIORITY = 5。即main线程默认优先级为5
- 守护线程:JAVA线程分两种,一种叫用户线程,一种叫守护线程。守护线程,当所有的用户线程都结束了之后,守护线程随着JVM一起结束工作,守护线程Daemon的作用就是为用户线程提供服务,最典型的守护线程例子就是GC。
/*
以下代码输出:
0 hh
*/
public class Test {
public static void main(String[] args) throws InterruptedException{
Runnable r = new Runnable() {
@Override
public void run() {
for(int i = 0;i < 5;i++) {
try {
System.out.println(i);
Thread.sleep(1000);
} catch (Exception e) {
// TODO: handle exception
return;
}
}
}
};
Thread t = new Thread(r);
//如果t是用户线程,那么输出将会是0 hh 1 2 3 4. 因为main线程和t线程是并发执行!!!
t.setDaemon(true);//将线程t设置为守护线程,在start之前设置,默认是用户线程
t.start();
System.out.println("hh");
}
}
- synchronized关键字(使用对象的内部锁和内部条件)。它自动提供一个锁及相关的“条件”,对于大多数需要显式锁的情况,这是很便利的
- 使用ReentrantLock类。使用锁时,锁应该是多个线程共享的变量
- LockObject.lock()获得锁,LockObject.unlock()释放锁。
- 一个锁对象可以有一个或多个相关的条件对象。可以用LockObject.newCondition()获得一个条件对象
- 使用conditionName.await()阻塞线程,并在其它线程中使用conditionName.signalAll()唤醒所有进程(conditionName为一个公用条件)。但是调用signalAll不会立即激活一个等待进程,它仅仅解除等待线程的阻塞,以便这些线程可以在当前线程退出同步方法后,通过竞争实现对对象的访问
- signal方法是从该条件的等待集中随机地选择一个线程,解除其阻塞状态
- 每一个对象有一个内部锁,并且该锁有一个内部条件
- synchronized方法或代码块使用wait()、notifyAll()、notify()阻塞和唤醒线程;Condition变量使用await、signalAll、signal阻塞和唤醒线程
- 如果一个类有一个静态同步方法,那么当该方法被调用时,ClassName.class对象的锁被锁住。因此,没有其他线程可以调用同一个类的这个或其他任何同步静态方法
- 阻塞队列:当试图向队列添加元素而队列已满,或是想从队列移出元素而队列为空的时候,阻塞队列导致线程阻塞。队列会自动地平衡负载。
- 阻塞队列的方法:
方法 | 正常动作 | 特殊情况下的动作 |
add | 添加一个元素 | 如果队列满,则抛出IllegalStateException |
element | 返回队列的头元素 | 如果队列空,则抛出NoSuchElementException |
offer | 添加一个元素并返回true | 如果队列满,则返回false(当带超时参数时,可阻塞) |
peek | 返回队列的头元素 | 如果队列空,则返回null |
poll | 移除并返回队列的头元素 | 如果队列空,则返回null(当带超时参数时,可阻塞) |
put | 添加一个元素 | 如果队列满,则阻塞 |
remove | 移除并返回队列的头元素 | 如果队列空,则抛出NoSuchElementException |
take | 移除并返回队列的头元素 | 如果队列空,则阻塞 |
- 高效的映射、集和队列:java.util.concurrent包提供了映射,有序集和队列的高效实现。比如,ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet和ConcurrentLinkedQueue
- 任何集合类都可以通过使用同步包装器变成线程安全的:
List<Integer> synList = Collections.synchronizedList(new ArrayList<>());
Map<Integer, Integer> synMap = Collections.synchronizedMap(new HashMap<Integer,Integer>());
- 并行异步计算框架:Callable+Future+ThreadPool
- 使用线程池的优点:
- 一个线程池中包含许多准备运行的空闲线程。将Runnable对象交给线程池,就会有一个线程调用run方法。当run方法退出时,线程不会死亡,而是在池中准备为下一个请求提供服务
- 减少并发线程的数目
方法 | 描述 |
newCachedThreadPool | 必要时创建线程;空闲线程会被保留60秒 |
newFixedThreadPool | 该程包含固定数量的线程;空闲线程会一直被保留 |
newSingleThreadExecutor | 只有一个线程的“池”,该线程顺序执行每一个提交的任务 |
newScheduledThreadPool | 用于预定执行而构建的固定线程池 |
newSingleThreadScheduledExecutor | 用于预定执行而构建的单线程池 |
- 当用完一个线程池的时候,调用shutdown,该方法启动该池的关闭序列,被关闭的执行器不再接受新的任务 。另一种方法是调用shutdownNow,该池将取消尚未开始的所有任务并试图中断正在运行的线程
- 使用线程池的步骤(也有其他方法创建线程池):
- 调用Executors类中静态方法newCachedThreadPool或newFixedThreadPool
- 调用submit提交Runnable或Callable对象(也可以直接调用execute(Runnable)直接执行线程任务)
- 如果想要取消一个任务,或如果提交Callable对象,那就要保存好submit返回的Future对象
- 当不再提交任务时,调用shutdown(不再接收新任务,但是现有的任务将继续执行直至完成)
- Callable接口与Runnable接口的区别:Callable接口是一个泛型接口,有一个泛型参数V,需要实现call函数,且call函数需要回一个V对象;Runnable接口非泛型接口,需实现run方法,返回值类型为void
- 当多个线程一起访问同一资源时,可能出现与预期不一致的结果。佐证代码如下(采用的是《Java语言程序设计》中的例子):
/*
以下代码的原意是想:
创建100个线程,每个线程都对于一个变量(初始为0)进行加1操作,那么线程结束后,这个变量应该变为100
由于未启用同步机制,因此最终的结果肯定不是100
要解决该问题的方法也很简单,只需要把对该变量进行操作的方法声明为同步方法即可
*/
package learningNotes;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class AccountWithoutSync {
private static learningNotes.AccountWithoutSync.AddAPennyTask.Account account = new learningNotes.AccountWithoutSync.AddAPennyTask.Account();
public static void main(String[] args) {
ExecutorService exector = Executors.newCachedThreadPool();//创建带缓冲的线程池
for(int i = 0;i < 100;i++) {
exector.execute(new AddAPennyTask());//往线程池中添加任务
}
exector.shutdown();
while(!exector.isTerminated()) {
}
System.out.println("what is balance?" + account.getBalance());
}
private static class AddAPennyTask implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
account.deposit(1);
}
private static class Account{
private int balance = 0;
public int getBalance() {
return balance;
}
public void deposit(int amount) {
int newBalance = balance + amount;
try {
Thread.sleep(5);//故意方法数据破坏的可能性,每一个线程休眠5ms
} catch (InterruptedException e) {
// TODO: handle exception
}
balance = newBalance;
}
}
}
}
- 使用synchronized方法或语句块比使用相互排斥的显式锁简单些,但使用显式锁对同步具有状态的线程更加直观和灵活。即显式锁更适合于线程间通信,因为显式锁可以创建条件变量
- 线程间相互合作实例(使用显式锁并使用了锁的条件变量):
/*
该程序模拟的是:
两个线程,一个向账户存款,一个向账户取款。
当取款时,若取款金额大于账户余额,则取款线程阻塞,并释放其拥有的锁
当存款时,每一次存款之后,都唤醒取款线程,让其再次去检查是否满足取款的条件
*/
package learningNotes;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadCooperation {
public static Account account = new Account();
public static void main(String[] args) {
// TODO Auto-generated method stub
System.out.println("Thread1\t\tThread2\t\t\tremains");
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.execute(new DepositTask());
executor.execute(new WithdrawTask());
executor.shutdown();
}
public static class DepositTask implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
try {
while (true) {
account.deposit((int)(Math.random() * 10) + 1);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
// TODO: handle exception
e.printStackTrace();
}
}
}
public static class WithdrawTask implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
while (true) {
account.withdraw((int) (Math.random() * 10) + 1);
}
}
}
public static class Account{
private static Lock lock = new ReentrantLock();
private static Condition newDeposit = lock.newCondition();
private int balance = 0;
public int getBalance() {
return balance;
}
public void withdraw(int amount) {
lock.lock();
try {
while (balance < amount) {
System.out.println("\t\twait for a deposit");
newDeposit.await();
}
balance -= amount;
System.out.println("\t\twithdraw " + amount + "\t\t" + getBalance());
} catch (InterruptedException e) {
// TODO: handle exception
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void deposit(int amount) {
lock.lock();
try {
balance += amount;
System.out.println("deposit " + amount + "\t\t\t\t" + getBalance());
newDeposit.signalAll();//因为本例中只有一个线程持有newDeposit条件,因此调用signal也是一样的效果
} finally {
// TODO: handle finally clause
lock.unlock();
}
}
}
}
- 条件中的await含义为:线程释放锁,并且线程停在await处,等获得了锁后,继续执行await后的代码。如上例,若把withdraw方法中的while循环改成if语句,经过await后,线程停留在await处。当被deposit方法的signalAll唤醒后,withdraw执行await后的代码。若为if,则执行if之外的语句(此时不能保证账户余额不小于取款金额);若为while,则需要进入while继续判断一次账户余额和取款金额的大小关系,再决定是取款还是继续await。因此上例中的while不能改成if
- 如果没有获取锁就尝试调用条件的方法,则会抛出IllegalMonitorStateException
- 不使用阻塞队列手动实现生产者/消费者模式:
package learningNotes;
import java.util.LinkedList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ConsumerProducer {
private static Buffer buffer = new Buffer();//创建缓冲区对象,Buffer为自定义的内部静态类
public static void main(String[] args) {
// TODO Auto-generated method stub
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.execute(new ProducerTask());
executor.execute(new ConsumerTask());
executor.shutdown();
}
public static class Buffer {
private static final int CAPACITY = 100;//设置缓冲区的大小
private LinkedList<Integer> queue = new LinkedList<>();//定义用于接收数据的数据结构
private static Lock lock = new ReentrantLock();//定义锁
private static Condition notEmpty = lock.newCondition();//定义锁的非空条件,当该条件满足时,消费者可以从queue中取出数据
private static Condition notFull = lock.newCondition();//定义锁的非满条件,当该条件满足时,生产者可以向queue添加数据
//定义用于生产者的write方法(向queue添加数据)
public void write(int val) {
lock.lock();
try {
while (queue.size() == CAPACITY) {//当当前queue的大小等于缓冲区自定义大小时,即队满,则阻塞线程并释放锁,直至notFull条件满足(其他线程唤醒notFull)
System.out.println("wait for notFull condition");
notFull.await();
}
//程序若进行到这,则表明queue此时未满,则可以往其中添加数据
//使用List的offer方法向queue末尾添加数据,也可以使用add
queue.offer(val);
notEmpty.signalAll();//由于往queue中添加了数据,此时queue一定非空,因此唤醒notEmpty条件
} catch (InterruptedException e) {
// TODO: handle exception
e.printStackTrace();
} finally {
// TODO: handle finally clause
lock.unlock();
}
}
@SuppressWarnings("finally")
//用于消费者的read方法,分析同上
public int read() {
int val = 0;
lock.lock();
try {
while (queue.isEmpty()) {
System.out.println("wait for notEmpty condition");
notEmpty.await();
}
val = queue.remove();//移除queue的头元素
notFull.signalAll();
} catch (InterruptedException e) {
// TODO: handle exception
e.printStackTrace();
} finally {
lock.unlock();
return val;
}
}
}
//ProducerTask任务类用于构建生产者任务
private static class ProducerTask implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
try {
int i = 1;
while (true) {
buffer.write(i);
System.out.println("producer writes " + i++ + ",queue.size = " + buffer.queue.size());
Thread.sleep((int)(Math.random() * 10000));
}
} catch (InterruptedException e) {
// TODO: handle exception
e.printStackTrace();
}
}
}
//ConsumerTask任务类用于构建消费者任务
private static class ConsumerTask implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
try {
while (true) {
System.out.println("consumer read " + buffer.read() + ",queue.size = " + buffer.queue.size());
Thread.sleep((int)(Math.random() * 10000));
}
} catch (InterruptedException e) {
// TODO: handle exception
e.printStackTrace();
}
}
}
}
- ArrayBlockingQueue:使用数组实现阻塞队列,必须指定一个容量,第二个参数为公平性(可选)
- LinkedBlockingQueue:使用链表实现阻塞队列,可以创建无边界(此时CAPACITY为Integer.MAX_VALUE)的或者有边界的LinkedBlockingQueue
- PriorityBlockingQueue:使用数组实现阻塞队列,可以创建无边界(初始CAPACITY为11,之后若满,会扩容)或者有边界(提供的参数只是初始的CAPACITY)的PriorityBlockingQueue
package learningNotes;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ConsumerProducerUsingBlockingQueue {
private static ArrayBlockingQueue<Integer> buffer = new ArrayBlockingQueue<>(100);//容量为100的阻塞队列
public static void main(String[] args) {
// TODO Auto-generated method stub
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.execute(new ProducerTask());
executor.execute(new ConsumerTask());
executor.shutdown();
}
private static class ProducerTask implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
try {
int i = 1;
while (true) {
buffer.put(i);
System.out.println("producer writes " + i++ + ",queue.size = " + buffer.size());
Thread.sleep((int)(Math.random() * 10000));
}
} catch (InterruptedException e) {
// TODO: handle exception
e.printStackTrace();
}
}
}
private static class ConsumerTask implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
try {
while (true) {
System.out.println("consumer read " + buffer.take() + ",queue.size = " + buffer.size());
Thread.sleep((int)(Math.random() * 10000));
}
} catch (InterruptedException e) {
// TODO: handle exception
e.printStackTrace();
}
}
}
}
- 信号量(Semaphore):指对共同资源进行访问控制的对象,使用信号量来限制同时访问一个共享资源的线程数。当信号量的许可数为1时,可以用信号量模拟一个相互排斥的锁。
- 通过调用信号量的acquire方法获得许可,若成功获得许可,则信号量的许可数减1;通过调用信号量的release方法释放许可,若成功释放许可,则信号量的许可数加1
- 方法interrupt(实例方法)按下列方式中断一个线程:当线程当前处于就绪或运行状态时,给它设置一个终端标志;当线程处于阻塞状态时,它将被唤醒并进入就绪状态,同时抛出异常java.lang.InterrupedException
- Collections提供了6个静态方法来将集合转成同步版本:
- synchronizedCollection(Collection c):Collection
- synchronizedList(List list):List
- synchronizedMap(Map map):Map
- synchronizedSet(Set set):Set
- synchronizedSortedMap(SortMap s):SortedMap
- synchronizedSortedSet(SortedSet s):SortedSet
- 同步合集可以很安全地被多个线程并发地修改和访问。但是迭代器具有快速失效的特性,这就意味着当使用一个迭代器对一个合集进行遍历,而其依赖的合集被另一个线程修改时,那么迭代器会抛出java.util.ConcurrentModificationException报错,该异常是RuntimeException的一个子类。为了避免这个错误,可以创建一个同步合集对象,并且在遍历它时获取对象上的锁。报错以及正确代码如下:
- 报错代码
/*
两个线程都试图遍历set,然后删除刚才遍历过的元素
因为两个线程是并发执行的,所以会报错
*/
public class Test{
public static void main(String[] args) throws InterruptedException{
Set<Integer> set = Collections.synchronizedSet(new HashSet<>());
set.addAll(Arrays.asList(new Integer[]{1,2,3,4}));
Runnable r1 = new Runnable() {
@Override
public void run() {
Iterator<Integer> iterator1 = set.iterator();
while (iterator1.hasNext()) {
System.out.println(iterator1.next() + Thread.currentThread().getName());
iterator1.remove();
}
}
},r2 = new Runnable() {
@Override
public void run() {
Iterator<Integer> iterator2 = set.iterator();
while (iterator2.hasNext()) {
System.out.println(iterator2.next() + Thread.currentThread().getName());
iterator2.remove();
}
}
};
new Thread(r1,"r1").start();
new Thread(r2,"r2").start();
}
}
/*
从输出结果可以看出只有一个线程成功遍历了set,当它遍历完后,另外一个线程所获得的
set的迭代器为空
*/
public class Test{
public static void main(String[] args) throws InterruptedException{
Set<Integer> set = Collections.synchronizedSet(new HashSet<>());
set.addAll(Arrays.asList(new Integer[]{1,2,3,4}));
Runnable r1 = new Runnable() {
@Override
public void run() {
synchronized (set) {
Iterator<Integer> iterator1 = set.iterator();
while (iterator1.hasNext()) {
System.out.println(iterator1.next() + Thread.currentThread().getName());
iterator1.remove();
}
}
}
},r2 = new Runnable() {
@Override
public void run() {
synchronized (set) {
Iterator<Integer> iterator2 = set.iterator();
while (iterator2.hasNext()) {
System.out.println(iterator2.next() + Thread.currentThread().getName());
iterator2.remove();
}
}
}
};
new Thread(r1,"r1").start();
new Thread(r2,"r2").start();
}
}