线程和进程:
线程是进程的组成部分,一个进程可以有多个线程,一个线程必须有父进程。线程之间共享内存非常容易,所以通常,创建线程的代价小的多。
Thread
类创建线程类public class FirstThread extends Thread {
private int i;
public void run() {
for( ; i < 50; i++) {
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args) {
for(var i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
if(i == 20) {
//创建并启动第一个线程
new FirstThread().start();
//创建并启动第二个线程
new FirstThread().start();
}
}
}
}
上面的程序中有三个线程,main, Thread-0, Thread-1, main方法决定主线程执行体。
Runnable
接口创建线程类Runnable
接口的实现类,并重写改接口的run方法,这个方法的方法体同样是线程的线程执行体。public class SecondThread implements Runnable{
private int i;
public void run() {
for( ; i < 50; i++)
{
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
public static void main(String[] args) {
for(var i =0 ; i <50; i++)
{
System.out.println(Thread.currentThread().getName() + " " + i);
if(i == 20) {
var p = new SecondThread();
var p2 = new SecondThread();
new Thread(p, "_one").start();
new Thread(p2, "_two").start();
}
}
}
}
接口Runnable中只有一个抽象方法。Callable也是。上面程序中,创建了两个SecondThread实例,这是因为采用这种方式,Runnable创建的多线程可以共享线程类的实例变量。
Callable
和Future
创建线程Callable接口是一个加强版的Runnable接口,他里面的call方法可以有返回值,可以声明抛出异常。
Future接口是用来代表Callable接口里call的返回值,提供了一个FutureTask实现类,Callable有泛型限制。
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class ThridThread {
public static void main(String[] args) {
var tt = new ThridThread();
FutureTask<Integer> task = new FutureTask<>((Callable<Integer>)() -> {
var i =0;
for( ; i < 50; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
return i;
});
for(var i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
if(i == 20) {
new Thread(task, "Callable").start();
}
}
try {
System.out.println(task.get());
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
使用Runnable或者Callable接口方式创建多线程的优缺点:
使用Thread的优缺点:
一般使用的都是接口创建多线程。
新建(New),就绪(Ready),运行(Running),阻塞(Blocked),死亡(Dead)。
新建:使用new关键字创建了一个线程之后,JVM为其分配了内存。
只能对新建状态的线程调用start,否则会引发异常。
就绪:当线程对象调用了start方法之后,处于这个状态的线程并没有开始运行,只是表示可以运行了。何时开始运行取决于JVM里线程调度器的调度。
运行:当程序正在执行的时候
阻塞:程序运行时占用的内存因为调度原因被其他程序占领时。
死亡:执行结束。
什么时候未出现阻塞?
发生下面的情况可以解除上面的阻塞
线程死亡
不要用start启用一个死亡的线程
join()
让一个线程等待另一个线程执行完。
public class JoinThread extends Thread {
public JoinThread(String name) {
super(name);
}
public void run () {
for (var i = 0; i < 100; i++) {
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args) throws Exception {
new JoinThread("NewThread").start();
for (var i = 0; i < 100; i++) {
if (i ==20) {
var jt = new JoinThread("被Join的线程");
jt.start();
jt.join();
}
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
上面代码的main线程调用用了jt的join方法,main线程处于阻塞状态,必须等jt结束才可以再继续执行。
join方法重载
后台线程
,在后台运行的线程,JVM的垃圾回收机制就是典型的后台线程。如果所有前台线程都死亡了,后台线程也会自动死亡。
public class DeamonThread extends Thread {
public void run () {
for (var i = 0; i < 500; i++) {
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args) {
var t = new DeamonThread();
//设置成后台线程
t.setDaemon(true);
t.start();
for(var i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
main线程结束的时候,Thread-0也结束了。
sleep()
线程睡眠
yield()
静态方法,可以让当前正在执行的线程暂停,但他不会阻塞线程,他只是将该线程转入就绪状态。只有优先级与当前线程相同,或者优先级比当前线程更高的处于就绪状态的线程才会获得执行的机会。所以有可能被暂停后立即执行了。
sleep和yield的区别:
InterruptedException
异常,yield没有改变线程优先级:
通过线程的setPriority(int newPriority)
来设置优先级,范围为1~10。
public class PriorityTest extends Thread{
public PriorityTest(String name)
{
super(name);
}
public void run() {
for(var i = 0; i < 50; i++) {
System.out.println(getName() + ",其优先级是:" + getPriority() + ",循环变量:" + i);
}
}
public static void main(String[] args) {
Thread.currentThread().setPriority(6);
for(var i = 0; i < 30; i++) {
if(i == 10) {
var low = new PriorityTest("低级");
low.start();
System.out.println("创建之初的优先级:" + low.getPriority());
low.setPriority(Thread.MIN_PRIORITY);
}
if(i == 20) {
var high = new PriorityTest("高级");
high.start();
System.out.println("创建之初的优先级:" + high.getPriority());
high.setPriority(Thread.MAX_PRIORITY);
}
}
}
}
在运行时,优先级高的有更多的执行机会。(建议使用静态常量来设置优先级)
当有多个线程同时访问一组数据或者文件,就有很大的概率出现错误结果。这时候就需要同步监视器来解决这个问题。
synchronized (obj) {
···
//同步代码块
}
线程开始执行同步代码块之前,必须先获得对同步监视器(obj)的锁定。
同步监视器阻止两个线程对同一个共享资源进行并发访问,因此通常推荐使用可能被并发访问的共享资源充当同步监视器。
public class Account {
private String accountNo;
private double balance;
public Account() {}
public Account(String accountNo, double balance) {
this.accountNo = accountNo;
this.balance = balance;
}
//setter和getter
public String getAccountNo() {
return this.accountNo;
}
public double getBalance() {
return this.balance;
}
public int hashCode() {
return accountNo.hashCode();
}
public boolean equals(Object obj) {
if(this == obj) {
return true;
}
if(obj != null && obj.getClass() == Account.class) {
var target = (Account) obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
//加锁的对象是this
public synchronized void draw(double drawAmount) {
if (balance >= drawAmount) {
System.out.println(Thread.currentThread().getName() + ", Successful withdraw " + drawAmount + " RMB.");
try {
Thread.sleep(1);
} catch (InterruptedException ie) {
ie.printStackTrace();
}
balance -= drawAmount;
System.out.println("\r" + Thread.currentThread().getName() + " Balance:" + balance);
} else {
System.out.println("Sorry, " + Thread.currentThread().getName() + ", your credit is running low.");
}
}
}
上面例子中,直接调用account的draw方法,加锁对象是account。符合 加锁-修改-释放锁 的逻辑。
释放同步监视器
程序无法显示释放同步监视器,会在以下几种情况自动释放
在以下情况不会释放同步监视器
class x {
//定义锁对象
private final ReentranLock lock = new ReentranLock();
//定义保证线程安全的方法
public void m() {
lock.lock();
try {
···
}
finally {
lock.unlock();
}
}
}
同步锁比其他两种方法更加灵活。Lock提供了用于非块结构的tryLock方法,以及试图获取可中断锁的lockInterruptibly方法,还有超时失效锁的tryLock(long, TimeUnit)方法。
ReentrantLock锁具有可重入性,也就是说,一个线程可以对已被加锁的ReentrantLock锁再次加锁。
一段被锁保护的代码,可以调用另一个被相同锁保护的方法。
死锁及常用出路策略
当两个线程相互等待对方释放同步监视器时就会发生死锁。程序不会抛出任何异常,也没有提示,所有线程一直处于阻塞状态。
class A {
public synchronized void foo(B b) {
System.out.println("当前线程名称:" + Thread.currentThread().getName() + "进入了A实例的foo方法");
try {
Thread.sleep(200);
}
catch (InterruptedException ie) {
ie.printStackTrace();
}
System.out.println("当前线程名称:" + Thread.currentThread().getName() + "试图调用B实例的last方法");
b.last();
}
public synchronized void last() {
System.out.println("进入了A类的last方法内部");
}
}
class B {
public synchronized void bar(A a) {
System.out.println("当前线程名称:" + Thread.currentThread().getName() + "进入了B实例的foo方法");
try {
Thread.sleep(200);
}
catch (InterruptedException ie) {
ie.printStackTrace();
}
System.out.println("当前线程名称:" + Thread.currentThread().getName() + "试图调用A实例的last方法");
a.last();
}
public synchronized void last() {
System.out.println("进入了A类的last方法内部");
}
}
public class DeadLock implements Runnable{
A a = new A();
B b = new B();
public void init() {
Thread.currentThread().setName("主线程");
a.foo(b);
System.out.println("进入主线程后···");
}
public void run() {
Thread.currentThread().setName("副线程");
b.bar(a);
System.out.println("进入副线程后···");
}
public static void main(String[] args) {
var dl = new DeadLock();
new Thread(dl).start();
dl.init();
}
}
出现下面的结果
当前线程名称:主线程进入了A实例的foo方法
当前线程名称:副线程进入了B实例的foo方法
当前线程名称:主线程试图调用B实例的last方法
当前线程名称:副线程试图调用A实例的last方法
程序无法向下执行了。是因为两个线程都在等待对方把锁解开。
应该尽量避免这种情况的发生
传统的线程通信,使用
wait()
,notify()
,notifyAll()
来通信,这三通方法由同步监视器来调用。第一个是让当前线程等待,直到另外两个方法来将他唤醒。另外两个就是唤醒在等待中的线程的,第二个随机唤醒一个,第三个是唤醒所有。
Condition
来控制线程通信如果程序不使用synchronized关键字来保证同步,而是用Lock,就不能使用传统的了。获得一个Condition对象,需要调用Lock对象的newCondition方法。
Condition提供了三个方法:
方法名 | 介绍 |
---|---|
await() | 类似于wait。有很多变体 |
signal() | 唤醒单个在等待的线程,也是任意的。只有当前线程放弃了对该Lock对象的锁定后(使用await),才可执行被唤醒的线程。 |
signalAll() | 唤醒所有在等待中的线程,只有当前线程放弃了对该Lock对象的锁定后,才可执行被唤醒的线程。 |
public class Account {
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private String accountNo;
private double balance;
private boolean flag = false;
public Account() {};
public Account(String accountNo, double balance) {
this.accountNo = accountNo;
this.balance = balance;
}
public String getAccountNo() {
return accountNo;
}
public void draw(double drawAmount) {
lock.lock();
try {
if (!flag) {
condition.await();
}
else {
if (!(drawAmount > balance)) {
System.out.println(Thread.currentThread().getName() + " 取钱:" + drawAmount);
balance -= drawAmount;
System.out.println("账户余额为:" + balance);
}
flag = false;
condition.signalAll();
}
}
catch (InterruptedException ie) {
ie.printStackTrace();
}
finally {
lock.unlock();
}
}
public void deposit(double depositAmount) {
lock.lock();
try {
if (flag) {
wait();
}
else {
System.out.println(Thread.currentThread().getName() + " 存钱:" + depositAmount);
balance += depositAmount;
System.out.println("账户余额为:" + balance);
flag = true;
condition.signalAll();
}
}
catch (InterruptedException ie) {
ie.printStackTrace();
}
finally {
lock.unlock();
}
}
public int hashCode() {
return accountNo.hashCode();
}
public boolean equals(Object obj) {
if(this == obj) {
return true;
}
if(obj != null && obj.getClass() == S16_6.syn.Account.class) {
var target = (S16_6.syn.Account) obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}
Condition与传统的相比逻辑基本相似。
- | 抛出异常 | 不同返回值 | 阻塞线程 | 制定超时时长 |
---|---|---|---|---|
队尾插入元素 | add(e) | offer(e) | put(e) | offer(e, time, unit) |
队头删除元素 | remove() | poll() | take() | poll(time, unit) |
获取,不删除元素 | elemrnt() | peek() | 无 | 无 |
BlockingQueue与其实现类
下面代码利用BlockingQueue来实现线程通信
class Producer extends Thread {
private BlockingQueue<String> bq;
public Producer(BlockingQueue<String> bq) {
this.bq = bq;
}
public void run() {
var strArr = new String[]{"java", "abc", "xyz"};
for (var i = 0; i < 999999; i ++) {
System.out.println(getName() + " 生产者准备生产集合元素");
try {
Thread.sleep(200);
bq.put(strArr[i % 3]);
}
catch(Exception e) {e.printStackTrace();}
System.out.println(getName() + "生产完成:" + bq);
}
}
}
class Consumer extends Thread {
private BlockingQueue<String> bq;
public Consumer(BlockingQueue<String> bq) {this.bq = bq;}
public void run() {
while(true) {
System.out.println(getName() + "消费者准备消费集合元素!");
try {
Thread.sleep(200);
bq.take();
}
catch (Exception e) {e.printStackTrace();}
System.out.println(getName() + "消费完成:" + bq);
}
}
}
public class BlockingQueueTest {
public static void main(String[] args) {
BlockingQueue<String> bq = new ArrayBlockingQueue<>(1);
new Producer(bq).start();
new Producer(bq).start();
new Producer(bq).start();
new Consumer(bq).start();
}
}
运行后,三个线程都想往队列里放元素,但是只由1个空间,某个线程放入了后,其他放入元素的线程就只能等待元素被取出。
java使用ThreadGroup来表示线程组。对线程组的控制相当于同时控制这批线程。用户创建的线程都属于制定的线程,没有制定线程组就属于默认线程组。线程创建的线程属于它本身的线程组。线程运行中途不能改变所属线程组。
下面的代码示范了线程组的一些用法。
class MyThread extends Thread {
//提供制定线程名的构造器
public MyThread(String name) {
super(name);
}
//提供制定线程名、线程组的构造器
public MyThread(ThreadGroup group, String name) {
super(group, name);
}
public void run() {
for(var i = 0; i < 20; i ++) {
System.out.println(getName() + " 线程的i变量" + i);
}
}
}
public class ThreadGroupTest {
public static void main(String[] args) {
//获取主线程所在的线程组,这是所有线程默认的线程组
ThreadGroup mainGroup = Thread.currentThread().getThreadGroup();
System.out.println("主线程的名字:" + mainGroup.getName());
System.out.println("主线程是否是后台线程:" + mainGroup.isDaemon());
new MyThread("主线程的线程").start();
var tg = new ThreadGroup("新线程组");
tg.setDaemon(true);
System.out.println("tg线程组是否是后台线程组:" + tg.isDaemon());
var tt = new MyThread(tg, "tg组的线程甲");
tt.start();
new MyThread(tg, "tg组的线程乙").start();
}
}
如果线程执行过程中抛出了一个未处理的异常,jvm在结束该线程之前会自动检查是否有对应的 Thread.UncaughtExceptionHandler 对象,如果有,会调用 void uncaughtException(Thread t, Throwable e)。如果没有找到,JVM会调用该线程所属的线程组的 uncaughtException() 方法来处理该异常。
class MyHandler implements Thread.UncaughtExceptionHandler{
//实现uncaughtException方法,该方法将处理线程未处理的异常
public void uncaughtException(Thread t, Throwable e) {
System.out.println(t + " 线程出现了异常 : " + e);
}
}
public class ExHandler{
public static void main(String[] args) {
Thread.currentThread().setUncaughtExceptionHandler(new MyHandler());
var a = 5/0;
System.out.println("program exit.");
}
}
运行结果:
Thread[main,5,main] 线程出现了异常 : java.lang.ArithmeticException: / by zero
可以发现,并没有执行最后一句输出命令,程序没有正常结束。
(1)、降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
(2)、提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行;
(3)方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换(cpu切换线程是有时间成本的(需要保持当前执行线程的现场,并恢复要执行线程的现场))。
(4)提供更强大的功能,延时定时线程池。
前三个返回一个 ExecutorService 对象,代表一个线程池,可以执行 Runnable 对象或 Callable 对象所代表的线程;中间两个返回 ScheduledExecutorService 线程池,他是 ExecutorService 的子类,可以再制定延迟后执行线程任务。最后两个是用于并行。
ExecutorService 代表了尽快执行的线程池,提供了三种方法:
ScheduledExecutorService 也提供了类似的方法,具体看API。
执行完后,用 shutdown() 或 shutdownNow() 来关闭,不会再接受新的任务,但是第一个方法会让原有的执行完,第二个会试图停止所有的任务,并返回等待执行的任务列表。
使用线程池来执行线程任务的步骤:
public class ThreadPoolTest {
public static void main(String[] args) throws Exception{
ExecutorService pool = Executors.newFixedThreadPool(6);
Runnable target = ()-> {
for(var i=0;i<50;i++){
System.out.println(Thread.currentThread().getName() + " 的 i 值为:" + i);
}
};
pool.submit(target);
pool.submit(target);
pool.shutdown();
}
}
创建 ForkJoinPool 实例之后,就可以调用 ForkJoinPool 的 submit(ForkJoinTesk task) 或 invoke(ForkJoinTask task) 方法来执行制定任务了。
其中 ForkJoinTask 代表一个可以并行、合并的任务。
ForkJoinTask 是一个抽象类,有两个子类,如上。RecursiveTask 代表有返回值的任务,另一个代表的是无返回值的。
class CalTask extends RecursiveTask<Integer>
{
//每个小任务最多只累加20个数
private static final int THRESHOLD = 20;
private int[] arr;
private int start;
private int end;
public CalTask(int[] arr, int start, int end) {
this.arr = arr;
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
int sum = 0;
if(end-start < THRESHOLD) {
for(var i = start; i <end; i++){
sum+=arr[i];
}
return sum;
}
else {
//拆分
int middle = (start + end)/2;
var left=new CalTask(arr, start, middle);
var right=new CalTask(arr,middle,end);
left.fork();
right.fork();
return left.join()+right.join();
}
}
}
public class Sum {
public static void main(String[] args) throws Exception{
var arr=new int[100];
var rand=new Random();
var total=0;
for(int i=0,len=arr.length;i<len;i++){
int tmp=rand.nextInt(20);
//求出总和并赋值
total+=(arr[i]=tmp);
}
System.out.println(total);
ForkJoinPool pool = ForkJoinPool.commonPool();
Future<Integer> future=pool.submit(new CalTask(arr,0,arr.length));
System.out.println(future.get());
pool.shutdown();
}
}
为每个线程都创建一个变量副本,以免造成冲突。提供了三个 public 方法。
class Account {
private ThreadLocal<String> name = new ThreadLocal<>();
public Account(String str) {
this.name.set(str);
System.out.println("---" + this.name.get());
}
public String getName() {return name.get();}
public void setName(String str) {
this.name.set(str);
}
}
class MyTest extends Thread{
private Account account;
public MyTest(Account acc, String name) {
super(name);
this.account = acc;
}
public void run() {
for(var i = 0; i < 10; i++) {
if(i==6){
account.setName(getName());
}
System.out.println(account.getName() + "账户的i值" + i);
}
}
}
public class ThreadLocalTest {
public static void main(String[] args) {
var at = new Account("初始名");
new MyTest(at, "Thread-First").start();
new MyTest(at, "Thread-Second").start();
}
}
总共有三个线程,每个线程都完全拥有自己的ThreadLocal变量。
改变的变量也是自己的。
java 集合比如:ArrayList,LinkedList,HashSet,TreeSet,HashMap,TreeMap等都是线程不安全的。
Collections 提供了几个方法可以吧他们包装成线程安全的集合。
上面的都是返回特定的线程安全的对象
//讲HashMap包装成线程安全的类
HashMap m = Collections.synchronizedMap(new HashMap());
主要分为两类:
public class PubSubTest {
public static void main(String[] args) {
//创建一个SubmissionPublisher作为发布者
SubmissionPublisher<String> publisher = new SubmissionPublisher<>();
//创建订阅者
MySubscriber<String> subscriber = new MySubscriber<>();
//注册订阅者
publisher.subscribe(subscriber);
//发布几个数据项
System.out.println("开发发布数据···");
List.of("Java", "Kotlin", "Go", "Erlang", "Swift", "Lua").forEach(im -> {
publisher.submit(im);
try{
Thread.sleep(500);
}
catch (Exception ignored) {}
});
publisher.close();
synchronized ("fkjava"){
try{
"fkjava".wait();
}
catch (Exception ignored) {}
}
}
}
class MySubscriber<T> implements Subscriber<T> {
private Flow.Subscription subscription;
@Override
public void onSubscribe(Flow.Subscription subscription){
this.subscription = subscription;
subscription.request(1);
}
@Override
public void onNext(T item) {
System.out.println("获取到数据:" + item);
subscription.request(1);
}
@Override
public void onError(Throwable t){
t.printStackTrace();
synchronized ("fkjava") {
"fkjava".notifyAll();
}
}
@Override
public void onComplete() {
System.out.println("订阅结束!");
synchronized ("fkjava") {
"fkjava".notifyAll();
}
}
}
运行结果:
开发发布数据···
获取到数据:Java
获取到数据:Kotlin
获取到数据:Go
获取到数据:Erlang
获取到数据:Swift
获取到数据:Lua
订阅结束!