学习笔记之《Java核心技术卷I》---- 第十四章 并发 + 《Java语言程序设计》(进阶篇)---- 第30章 多线程和并行程序设计

本文深入探讨了Java中的线程管理和并发编程,包括线程的五种状态,创建线程的步骤,线程的中断、同步机制、阻塞队列以及线程池的使用。同时,讲解了Callable与Runnable接口的区别,强调了线程安全和避免并发问题的重要性,并给出了信号量和条件变量在多线程协作中的应用实例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  • 一个程序同时执行多个任务,通常,每一个任务称为一个线程
  • 进程与线程的本质区别在于每个进程拥有自己的一整套变量,而线程则共享数据。共享数据使线程之间的通信比进程之间的通信更有效、更容易
  • 创建线程的步骤: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
  • 线程的五种状态:
  1. 新建。即线程被创建
  2. 就绪。调用线程的start方法
  3. 运行。线程运行Runnable对象的run方法
  4. 阻塞。join:等待目标完成后转成就绪状态 sleep:等待超时后转成就绪状态 wait:等待获得信号后转成就绪状态  
  5. 结束。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的计时版
  • 线程被终止的情况:
  1. 正常终止。run方法正常退出而自然死亡
  2. 意外死亡。因为一个没有捕获的异常而终止了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");
	}
}
  • 线程优先级:
  1. 使用getPriority()和setPriority()获得当前线程的优先级和设置当前线程的优先级
  2. MIN_PRIORITY = 1,MAX_PRIORITY = 10
  3. 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");
	}
}
  • 防止代码块受干扰的机制:
  1. synchronized关键字(使用对象的内部锁和内部条件)。它自动提供一个锁及相关的“条件”,对于大多数需要显式锁的情况,这是很便利的
  2. 使用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包提供了映射,有序集和队列的高效实现。比如,ConcurrentHashMapConcurrentSkipListMapConcurrentSkipListSetConcurrentLinkedQueue
  • 任何集合类都可以通过使用同步包装器变成线程安全的:
List<Integer> synList = Collections.synchronizedList(new ArrayList<>());
Map<Integer, Integer> synMap = Collections.synchronizedMap(new HashMap<Integer,Integer>());
  • 并行异步计算框架:Callable+Future+ThreadPool
  • 使用线程池的优点:
  1. 一个线程池中包含许多准备运行的空闲线程。将Runnable对象交给线程池,就会有一个线程调用run方法。当run方法退出时,线程不会死亡,而是在池中准备为下一个请求提供服务
  2. 减少并发线程的数目
  • 执行器Executors类中五个创建线程池方法:
方法描述
newCachedThreadPool必要时创建线程;空闲线程会被保留60秒
newFixedThreadPool该程包含固定数量的线程;空闲线程会一直被保留
newSingleThreadExecutor只有一个线程的“池”,该线程顺序执行每一个提交的任务
newScheduledThreadPool用于预定执行而构建的固定线程池
newSingleThreadScheduledExecutor用于预定执行而构建的单线程池
  • 当用完一个线程池的时候,调用shutdown,该方法启动该池的关闭序列,被关闭的执行器不再接受新的任务 。另一种方法是调用shutdownNow,该池将取消尚未开始的所有任务并试图中断正在运行的线程
  • 使用线程池的步骤(也有其他方法创建线程池):
  1. 调用Executors类中静态方法newCachedThreadPool或newFixedThreadPool
  2. 调用submit提交Runnable或Callable对象(也可以直接调用execute(Runnable)直接执行线程任务)
  3. 如果想要取消一个任务,或如果提交Callable对象,那就要保存好submit返回的Future对象
  4. 当不再提交任务时,调用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();
			}
		}
	}
}
  •  三类阻塞队列:
  1. ArrayBlockingQueue:使用数组实现阻塞队列,必须指定一个容量,第二个参数为公平性(可选)
  2. LinkedBlockingQueue:使用链表实现阻塞队列,可以创建无边界(此时CAPACITY为Integer.MAX_VALUE)的或者有边界的LinkedBlockingQueue
  3. 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个静态方法来将集合转成同步版本:
  1. synchronizedCollection(Collection c):Collection
  2. synchronizedList(List list):List
  3. synchronizedMap(Map map):Map
  4. synchronizedSet(Set set):Set
  5. synchronizedSortedMap(SortMap s):SortedMap
  6. 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();
	}
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值