1. 线程概述
0. 一个任务通常就是一个程序, 每个运行中的程序就是一个进程. 当一个程序运行时, 内部可能包含了多个顺序执行流, 每个顺序执行流就是一个线程.
1. 进程是系统进行资源分配和调度的一个独立单位. 三个特征:
- 独立性: 进程可以拥有自己独立的资源, 每个进程都有自己独立的地址空间. 没有经过进程本身允许的情况下, 一个用户进程不可以直接访问其他进程的地址空间.
- 动态性: 进程与程序的区别在于, 程序只是一个静态的指令集合, 进程是在系统中活动的指令集合. 在进程中加入了时间的概念. 进程有生命周期和各种不同的状态.
- 并发性:多个进程可以在单个处理器上并发执行, 多个进程之间不互相影响.
2.
*并发性(concurrency) 和 并行性(parallel): 并行指在同一时刻有多条命令在多个处理器上同时执行; 并发指同一时刻只有一条命令执行, 但多个进程指令呗快速轮换执行, 使得在宏观上具有多个进程同时执行的效果.
3. 多线程扩展了多进程的概念, 使得同一个进程可以同时并发处理多个任务. 线程(Thread)也被称作轻量级进程(Lightweight Process), 线程是进程的执行单元.
4. 线程可以拥有自己的堆栈, 自己的程序计数器和局部变量, 但不拥有系统自语那, 它和父进程的其他线程共享该进程所拥有的全部资源.
5. 线程的执行是抢占式的, 即当前运行的线程在任何时候都可能被挂起, 以便另一线程运行.
6. main()方法对应的是Java程序默认的主线程.
2. 线程的创建和使用
0. java.lang.Thread: 线程对象是Thread类或其子类的实例.
2.1 继承Thread类创建线程类
0. 步骤:
- 定义Thread类的子类, 重写该类的run()方法, 该run()方法代表了线程要完成的任务, 因此run()方法称为线程执行体.
- 创建Thread子类的实例, 即创建线程对象.
- 调用线程对象的start()方法启动线程.
1. 例子(FirstThread.java).
public class FirstThread extends Thread
{
private int i;
public void run()
{
for (; i < 100; i++)
{
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args)
{
for (int i = 0; i < 100; i++)
{
System.out.println(Thread.currentThread().getName()
+ " " + i);
if (i == 20)
{
FirstThread a = new FirstThread();
a.start();
new FirstThread().start();
}
}
}
}
# Thread.currentThread(): Thread类的静态方法, 返回当前正在执行的线程对象.
# getName(): Thread类的实例方法, 返回调用该方法的线程名.
# Thread-0 和 Thread-1两个线程输出的i变量不连续-- i变量是FirstThread的实例属性, 而不是局部变量, 但因为程序每次创建线程对象时都需要创建一个FirstThread对象, 所以Thread-0和Thread-1不能共享该实例属性.
2. 可以用setName(String name) 和 getName() 方法设定和获取指定线程的名字. 默认主线程名"main", 用户启动的线程名字一次"Thread-0", "Thread-1"....
3. 继承Thread类创建的线程类, 多个线程之间不能共享线程类的实例变量.
2.2 实现Runnable接口创建线程类
0. 步骤:
- 定义Runnable接口的实现类(重写run()方法).
- 创建Runnable的对象, 并以此对象作为Thread的target来创建Thread对象. 创建时可以指定线程名.
1. 例子(SecondThread).
public class SecondThread implements Runnable
{
private int i;
public void run()
{
for (; i < 100; i++)
{
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
public static void main(String[] args)
{
for (int i = 0; i < 100; i++)
{
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 20)
{
SecondThread st = new SecondThread();
new Thread(st, "THE-ONE").start();
new Thread(st, "THE-2ONE").start();
}
}
}
}
# 两个子线程的i变量是连续的, 也就是采用Runnable接口的方式创建的多个线程可以共享线程的实例属性. 这是应为这种方式下, 程序所创建的Runnable接口的对象只是线程的target, 而多个线程可以共享一个target, 所以多个线程可以共享同一个线程类(实际上应该是线程的target类)的实例属性. (
? 相当于对同一个任务(target) 用了多线程来实现.)
2.3 使用Callable和Future创建线程
0. java.util.concurrent.Callable接口提供了一个call()方法可以作为线程执行体. call()方法与java.lang.Runnbale的run()方法类似但更强大.
1. 因为Callabe不是Runnable的子接口, 所以Callable对象不能作为Thread的target. 但Java提供了一个java.util.concurrent.Future接口代表call()方法的返回值, 并为Future提供了FutureTask实现类, FutureTask即实现了Future又实现了Runnable, 可以作为Thread类的target.
2. Future接口有如下方法控制他关联的Callable任务:
- boolean cancel(boolean mayInterruptIfRunning): 试图取消该Future里关联的Callable任务.
- V get(): 返回Callable任务里call()方法的返回值. 此方法将导致程序阻塞, 必须等到子线程结束后才会得到返回值.
- V get(long timeout, TimeUnit unit): 最多阻塞timeout和unit指定的时间, 如果指定时间后Callable任务还没有返回值, 将会抛出TimeoutException.
- boolean isCancelled(): 如果在Callable任务正常完成之前被取消, 则返回true.
- boolean isDone()
3. Callable接口有泛型限制, Callable接口里的泛型形参类型与call()方法返回值类型相同.
4. 步骤:
- 创建Callable接口的实现类, 实现call()方法.
- 创建Callable的实例, 使用FutureTask类来包装Callable对象.
- 使用FutureTask对象作为Thread对象的target创建并启动新线程.
- 调用FutureTask对象的get()方法获取子线程执行结束后的返回值.
5. 例子.
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class ThirdThread implements Callable<Integer>
{
public Integer call()
{
int i = 0;
for (; i < 100; i++)
{
System.out.println(Thread.currentThread().getName() + " " + i);
}
return i;
}
public static void main(String[] args)
{
ThirdThread rt = new ThirdThread();
FutureTask<Integer> task = new FutureTask<Integer>(rt);
for (int i = 0; i < 100; i++)
{
System.out.println(Thread.currentThread().getName() + "的循环变量i的值: " + i);
if (i == 20)
{
new Thread(task, "有返回值的线程").start();
}
}
try
{
System.out.println("子线程的返回值: " + task.get());
}
catch (Exception ex)
{
ex.printStackTrace();
}
}
}
2.4 创建线程的三种方式对比
- 使用Runnable或Callable创建的多个线程可以共享同一个target对象. 还可以继承其他父类.
- ? 继承Thread类创建多线程因为线程已经继承了Thread类, 所以不能再继承其他父类.
3. 线程的生命周期
线程声明周期包括: 新建(New)→就绪(Runnable)→运行(Running)→阻塞(Blocked)→死亡(Dead)五种状态. 线程状态可能多次在Running和Blocked之间切换.
3.1 新建和就绪状态
0. 启动线程应该调用start()方法, 如果调用run()方法则会使该线程一直处于Running状态, 不能和其他线程并发执行.
1. 线程对象调用start()之后, 该线程处于Runnable状态, 但是并没有运行, 什么时候运行取决于JVM里线程调度器的调度.
2. 如果希望调用子线程的start()方法后线程立即执行, 可以用Thread.sleep(1)来让当前运行的线程(主线程)睡眠1毫秒. (因为这1毫秒内CPU不会空闲, 它去执行另一个Runnable状态的线程.)
3.2 运行和阻塞状态
0. 现代桌面和服务器OS都采用抢占式调度策略, 但有的小型设备如手机可能采用"协作式调度策略"--只有当一个线程调用了它的sleep()或yield()方法后才会放弃所占用的资源(即该线程必须主动放弃).
1. 进入阻塞状态的条件和解除方法:
- sleep()
- 线程调用了阻塞式的IO方法, 在该方法返回之前, 该线程被阻塞.
- 线程试图获得一个"同步监视器", 但该同步监视器被其他线程所持有. (成功获得同步监视器后解除阻塞.)
- 线程在等待某个通知(notify).
- 程序调用suspend()方法将该线程挂起. (调用resume()方法恢复.)
2. 被阻塞的线程的阻塞接触后, 进入就绪状态(Runnable)不是直接进入运行状态(Running).
3. 通常运行状态和就绪状态不受程序控制, 但yield()方法可以让运行状态的线程进入就绪状态.
3.3 线程死亡
0. 线程结束方式有三种, 结束后就是Dead状态.
- run()或call()方法执行完.
- 线程抛出一个未捕获的Exception或Error.
- 直接调用该线程的stop()方法.(容易导致死锁, 不推荐.)
1.
* 主线程结束时, 其他线程不受任何影响. 子线程一旦启动起来就有和主线程一样的地位.
2. 线程对象的isAlive()方法返回它的状态: true代表处于就绪、运行或阻塞状态, false代表新建或死亡.
3. 只能对新建状态的线程用start(), 但对新建状态的线程调用两次start()或对一个死亡的线程调用start()会报IllegalThreadStateException.
4. 控制线程
4.1 join线程
0. 在程序执行流中调用其他线程的join()方法时, 调用线程(主调)被阻塞, 直到被join()方法加入的join线程执行完毕.
1. 例子.
public class JoinThread extends Thread
{
public JoinThread(String name)
{
super(name);
}
public void run()
{
for (int i = 0; i < 100; i++)
{
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args) throws Exception
{
new JoinThread("new thread").start();
for (int i = 0; i < 100; i++)
{
if (i == 20)
{
JoinThread jt = new JoinThread("the joined thread");
jt.start();
jt.join();
}
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
2. join()方法3种重载形式:
- join()
- join(long millis): 等待被join的线程的时间最长为millis毫秒. (超时则join进的线程和调用join线程的线程并发.)
- join(long millis, int nanos): 时间最长为(millis毫秒+nanos毫微秒(纳秒)). * 毫秒, ms, 千分之一秒; 纳秒, ns, 十亿分之一秒.
4.2 后台线程(Deamon Thread)
0. "后台线程"是在后台运行的, 为其他线程提供服务的线程(又叫"守护线程", "精灵线程".) JVM的垃圾回收线程就是后台线程. (Deamon, 是后台进程, 守护进程.)
1. 如果所有前台线程都死亡, 则后台线程自动死亡. 当整个虚拟机中只剩下后台线程时, 程序就没有继续运行的必要了, 所以虚拟机也就退出了.
2. Thread对象的setDaemon(true)方法将指定线程设置成后台线程.
3. 例子.
public class DaemonThread extends Thread
{
public void run()
{
for (int i = 0; i < 1000; i++)
{
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args)
{
DaemonThread t = new DaemonThread();
t.setDaemon(true);
t.start();
for (int i = 0; i < 10; i++)
{
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
4. 前台线程创建的线程默认是前台线程, 后台线程创建的是后台线程. Thread的isDaemon()方法判断是否后台.
5.
* 前台线程死亡后, JVM通知后台线程死亡, 这还要个时间.
4.3 线程睡眠: sleep
0. sleep()是Thread的静态方法, 让线程暂停一段时间, 进入阻塞(blocked状态). 两种重载形式:
- static void sleep(long millis)
- static void sleep(long millis, int nanos): millis毫秒 + nanos毫微秒.
1. sleep() 方法声明抛出了InterruptedException异常, 所以调用sleep()时要么捕捉, 要么显式声明抛出该异常.
2. 例子.
import java.util.Date;
public class SleepTest
{
public static void main(String[] args)
throws Exception
{
for (int i = 0; i < 10; i++)
{
System.out.println("当前时间" + new Date());
Thread.sleep(1000);
}
}
}
4.4 线程让步: yield
0. yield也是Thread的静态方法.
1. yield将线程转为就绪状态. 只是让线程暂停一下, 如果程序中不存在优先级比该线程高或相同的线程, 则此让步的线程继续运行; 如果有, 另算 (给优先级更高的线程让步).
2. sleep() 方法声明抛出InterruptedException异常, yield() 方法没抛出异常.
3. 例子.
public class YieldTest extends Thread
{
public YieldTest(String name)
{
super(name);
}
public void run()
{
for (int i = 0; i < 50; i++)
{
System.out.println(getName() + " " + i);
if (i == 20)
{
Thread.yield();
}
}
}
public static void main(String[] args) throws Exception
{
YieldTest yt1 = new YieldTest("HIGH");
//yt1.setPriority(Thread.MAX_PRIORITY);
yt1.start();
YieldTest yt2 = new YieldTest("LOW");
//yt2.setPriority(Thread.MIN_PRIORITY);
yt2.start();
}
}
4.5 改变线程优先级
0. 优先级高的线程更多执行机会.
1. 线程的默认优先级与创建它的父线程优先级相同. main默认普通优先级.
2. Thread类提供setPriority(int newPriority)和getPriority()方法. 其中newPriority参数是1到10的整数, 也可以使用Thread类的3个静态常量:
- MAX_PRIORITY: 值为10.
- MIN_PRIORITY: 1.
- NORM_PRIORITY: 5.
3. 不同系统提供的优先级不同(win2000只提供了7种优先级), 不一定能和Java的10个优先级对应, 所以为了更好的可移植性, 用静态常量.
4. 例子.
public class PriorityTest extends Thread
{
public PriorityTest(String name)
{
super(name);
}
public void run()
{
for (int i = 0; i < 50; i++)
{
System.out.println(getName() + "'s priority is " + getPriority() + ", i's value is " + i);
}
}
public static void main(String[] args)
{
Thread.currentThread().setPriority(6);
for (int i = 0; i < 30; i++)
{
if (i == 10)
{
PriorityTest low = new PriorityTest("LOW");
low.start();
System.out.println("the INIT priority is: " + low.getPriority());
low.setPriority(Thread.MIN_PRIORITY);
}
if (i == 20)
{
PriorityTest high = new PriorityTest("HIGH");
high.start();
System.out.println("the INIT priority is: " + high.getPriority());
high.setPriority(Thread.MAX_PRIORITY);
}
}
}
}
5. 线程同步
5.1 线程安全问题
如"银行取钱问题"(几个线程并发操作同一对象).
5.2 同步代码块
0. 用同步监视器解决上述问题.
synchronized(obj)
{
同步代码块
}
# obj作为同步监视器.
# 线程执行同步代码块之前, 先对obj进行锁定(如果已被锁定, 则同步代码块不能运行)在执行, 执行结束后释放锁定.
# 可以保证同一对象同一时刻最多由一个线程处理.
1. 用法.
public void run()
{
synchronized(obj)
{
//同步代码块
}
}
5.3 同步方法
0. 与5.2类似, 用synchronized关键字修饰某个方法, 则此方法叫同步方法.
1. 同步方法的同步监视器无须显示声明. 同步方法的同步监视器是this(即这个对象本身),
2. 不可变类的对象状态是不可变的, 所以它的对象总是线程安全的.
3. 例子.
public synchronized void draw(double drawAmount)
{
if (balance >= drawAmount)
{
System.out.println(Thread.currentThread().getName()
+ "取钱成功!吐出马奶: " + drawAmount);
balacne -= drawAmount;
System.out.println("\t余额: " + balance);
}
else
{
System.out.println(Thread.currentThread().getName()
+ "取钱失败,钱不够.");
}
}
4. aynchronized关键字只能修饰方法和代码块, 不能修饰构造器和Field.
5.4 释放同步监视器的锁定
0. 程序不能显式释放对同步监视器的锁定.
1. 释放锁定的情况:
- 同步方法/同步代码块执行结束时.
- 同步方法/同步代码块中遇到break or return时.
- 同步方法/同步代码块出现未处理的Error or Exception 导致异常终止时.
- *当前线程执行同步方法/同步代码块时, 程序执行了同步监视器的wait()方法, 则当前线程暂停, 并释放同步监视器.
- *线程执行同步方法/同步代码块时, 程序调用Thread.sleep()或Thread.yield()方法来暂停当前线程时, 不释放同步监视器的锁定.
- 线程执行同步代码块时, 其他线程调用了该线程的suspend()方法将该线程挂起, 不释放.
5.5 同步锁(Lock)
0. 还可以显式定义同步锁对象实现同步, 同步锁使用Lock对象充当.
1. 某些锁能对共享资源并发访问, 如ReadWriteLock读写锁.
java.util.concurrent.locks.Lock和 java.util.concurrent.locks.ReadWriteLock是两个根接口, 有ReentrantLock(可重入锁)和ReentranReadWriteLock实现类.
2. ReentrantLock是可重入锁, 即一个线程可以对一个已被加锁的ReentrantLock锁再次加锁, ReentrantLock对象会追踪lock()方法的嵌套调用.
3.?一段被锁保护的代码可以调用另一个被相同锁保护的方法.
4. 用法.
class X
{
private final ReentrantLock lock = new ReentrantLock();
...
public void m()
{
lock.lock();
try
{
...
}
finally
{
lock.unlock();
}
}
}
5.6 死锁
0. 两个线程相互等待对方释放同步监视器对象时发生死锁.
1. "死锁"和"程序阻塞"看上去一样, 其实不一样.
2. 例子.
class A
{
public synchronized void foo(B b)
{
System.out.println("当前线程名: " + Thread.currentThread().getName() + "进入了实例A的foo方法");
try
{
Thread.sleep(200);
}
catch (InterruptedException ex)
{
ex.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的bar方法");
try
{
Thread.sleep(200);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
System.out.println("当前线程名: " + Thread.currentThread().getName() + "企图调用A实例的last方法");
a.last();
}
public synchronized void last()
{
System.out.println("进入了B类的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)
{
DeadLock dl = new DeadLock();
new Thread(dl).start();
dl.init();
}
}
运行后输出
为毛先输出"主线程..."啊
?
6. 线程通信
程序不能控制线程的轮换执行, 但可以通过一些机制保证线程协调运行.
6.1 传统的线程通信
0.
Object类提供了wait(), notify()和notifyAll()三个方法实现线程通信, 它们不属于Thread类.
- wait(): 导致当前线程等待, 直到其他线程调用该同步监视器的notify()或notifyAll()方法来唤醒该线程(也可以指定等待时间). * 调用wait()方法会释放当前线程对同步监视器的锁定.
- notify(): 唤醒在此同步监视器上等待的单个线程. 如果多个线程都是在此同步监视器上等待, 则随机唤醒一个. 只有当前线程放弃对该监视器的锁定后(用wait()方法), 才可以执行被唤醒的线程.
- notifyAll(): 唤醒在此同步监视器上等待的所有个线程. 只有当前线程放弃对该监视器的锁定后(用wait()方法), 才可以执行被唤醒的线程.
1. wait(), notify()和notifyAll()方法由同步监视器对象来调用:
- 对synchronized修饰的同步方法, 因为默认的实例(this)是同步监视器本身, 所以可以直接使用这三个方法.
- 对synchronized修饰的同步代码块
2. wait()方法抛出InterreptedException异常, 像sleep()一样.
6.2 使用Condition控制线程通信
0. 如果不使用synchronized关键字保证同步, 则不能使用wait(), notify()和notifyAll()方法进行通信. 使用Lock对象保证同步时, 用Condition类保持协调.
1. Condition实例被绑定在Lock实例上, 要获得它用Lock对象的newCondition()方法即可.
2. Condition的三个方法:
- await(): 类似隐式同步监视器上的wait()方法, 导致当前线程等待, 直到其他线程调用该Condition的signal()或signalAll()方法来唤醒该线.
- signal(): 唤醒在此Lock对象上等待的单个线程. 如果多个线程都是在此Lock对象上等待, 则随机唤醒一个. 只有当前线程放弃对该Lock对象的锁定后(用await()方法), 才可以执行被唤醒的线程.
- signalAll(): 唤醒在此Lock对象上等待的所有线程. 只有当前线程放弃对该Lock对象的锁定后, 才可以执行被唤醒的线程.
3. 例子.
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.Condition;
public class Account
{
private final Lock lock = new ReentrantLock();
private final Condition cond = 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 void setAccountNo(String accountNo)
{
this.accountNo = accountNo;
}
public String getAccountNo()
{
return accountNo;
}
public double getBalance()
{
return balance;
}
public void draw(double drawAmount)
{
lock.lock();
try
{
if (!flag)
{
cond.await();
}
else
{
System.out.println(Thread.currentThread().getName() + "取钱: " + drawAmount);
balance -= drawAmount;
System.out.println("\t账户余额为: " + balance);
flag = false;
cond.signalAll();
}
}
catch(InterruptedException ex)
{
ex.printStackTrace();
}
finally
{
lock.unlock();
}
}
public void deposit(double depositAmount)
{
lock.lock();
try
{
if (flag)
{
cond.await();
}
else
{
System.out.println(Thread.currentThread().getName() + "存款: " + depositAmount);
balance += depositAmount;
System.out.println("账户余额为: " + balance);
flag = true;
cond.signalAll();
}
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
finally
{
lock.unlock();
}
}
// 此处省略了hashCode()和equals()方法.
}
6.3 使用阻塞队列(BlockingQueue)控制线程通信
0. BlockingQueue是Queue的子接口, 但通常不作为容器, 而作为线程同步的工具.
1. BlockingQueue队列特征: 当生产者线程试图向BlockingQueue中放入元素时, 如果队列已满, 则线程阻塞; 消费者线程试图从队列中取出元素时, 若已空, 阻塞.
2. BlockingQueue的方法:
- put(E e): 把e元素放入BlockingQueue, 如果队列已满, 阻塞.
- take(): 从BlockingQueue的头部取出元素, 若已空, 阻塞.
- 队尾插入元素: add(E e), offer(E e), put(E e)方法. 如果已满, 分别: 抛出异常, 返回false, 阻塞队列.
- 队头删除并返回元素: remove(), poll(), take()方法. 如果已空, 分别: 抛出异常, 返回false, 阻塞队列.
- 队头取出但不删除元素: element(), peek()方法. 如果已空, 分别: 抛出异常, 返回false.
3. 例子.
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ArrayBlockingQueue;
class Producer extends Thread
{
private BlockingQueue<String> bq;
public Producer(BlockingQueue<String> bq)
{
this.bq = bq;
}
public void run()
{
String[] strArr = new String[]
{
"Java",
"Struts",
"Spring"
};
for (int i = 0; i < 9999999; i++)
{
System.out.println(getName() + "生产者准备生产集合元素!");
try
{
Thread.sleep(200);
bq.put(strArr[i % 3]);
}
catch(Exception ex)
{
ex.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 ex)
{
ex.printStackTrace();
}
System.out.println(getName() + "消费完成: " + bq);
}
}
}
public class BlockingQueueTest2
{
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();
}
}
# 运行结果有问题.
7. 线程组和未处理的异常
7.1 线程组
0. 用ThreadGroup表示线程组, 对线程进行分类管理. 所有线程都属于指定组(不显示指定则为默认组). 默认子线程和父线程处于同一个线程组(线程a属A组, a创建b且不指定线程组, 则b属A组).
1. 线程组一旦指定, 不能改.
2. Thread提供了构造器设置新建线程属于哪个线程组:
- Thread(ThreadGroup group, Runnable target): 以target的run()方法作为线程执行体创建新线程, 属于group线程组.
- Thread(ThreadGroup group, Runnable target, String name): 以target的run()方法作为线程执行体创建名为name的新线程, 属于group线程组.
- Thread(ThreadGroup group, String name): 创建新线程, 名为name, 属于group线程组.
3. 所属线程组不能变更, 所以Thread有getThreadGroup()方法没有setThreadGroup().
4. ThreadGroup的构造器:
- ThreadGroup(String name):
- ThreadGroup(ThreadGroup parent, String name): 以指定的父线程组创建名为name的线程组.
5. ThreadGroup类的常用方法:
- int activeCount(): 返回此组中活动线程的数目.
- interrupt(): 中断此组所有线程.
- isDaemon(): 判断改组是否为后台线程组.
- setDaemon(boolean daemon): 设为后台线程组(特征: 后台线程组的最后一个线程执行结束或被销毁后, 后台线程组自动销毁).
- setMaxPriority(int pri): 设置线程组最高优先级.
- 例子.
class MyThread extends Thread { public MyThread(String name) { super(name); } public MyThread(ThreadGroup group, String name) { super(group, name); } public void run() { for (int 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(); ThreadGroup tg = new ThreadGroup("新线程组"); tg.setDaemon(true); System.out.println("tg是否是后台线程组: " + tg.isDaemon()); MyThread tt = new MyThread(tg, "tg组的线程甲"); tt.start(); new MyThread(tg, "tg组的线程乙").start(); } }
7.2 异常处理
0. 用Thread.UncaughtExceptionHandler的uncaughtException()方法对线程组内未处理的异常进行处理.
1. Thread.UncaughtExceptionHandler是Thread类的静态内部接口, 它只有一个方法: void uncaughtExceptionHandler(Thread t, Throwable e) , t代表出现异常的线程, e代表该线程抛出的异常.
2. 如果线程执行过程中抛出了一个未处理的异常, JVM在结束该线程之前会自动查找是否有对应的Thread.UncaughtExceptionHandler对象, 如果有, 则调用该对象的uncaughtException(Thread t, Throwable e)方法来处理该异常.
3. Thread类提供了两种设置异常处理器的方法:
- static setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh): 为该线程类的所有线程实例设置默认的异常处理器.
- setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh): 为指定的线程实例设置异常处理器.
4. ThreadGroup类实现了Thread.UncaughtExceptionHandler接口, 所以每个线程所属的线程组都会作为默认的异常处理器. 一个线程抛出异常时, JVM先查找该异常对应的异常处理器, 如果没有, 则JVM调用该线程所属的线程组对象的uncaughtException()方法来处理该异常. 线程组处理异常的默认流程:
- 如果该线程组有父线程组, 调用父线程组的uncaughtException()方法处理.
- 如果该线程实例所属的线程类有默认的异常处理器, 调用该异常处理器处理异常.
- 如果该异常对象是ThreadDeath的对象, 则不作任何处理; 否则, 将异常跟踪栈的信息打印到System.err错误输出流, 并结束该线程.
5. 例子.
class MyExHandler implements Thread.UncaughtExceptionHandler
{
public void uncaughtException(Thread t, Throwable e)
{
System.out.println(t + " -X- " + e);
}
}
public class ExHandler
{
public static void main(String[] args)
{
Thread.currentThread().setUncaughtExceptionHandler(new MyExHandler());
int a = 5 / 0;
System.out.println(" RIGHT ");
}
}
8. 线程池
0. 使用线程池可以提供性能, 尤其是要创建大量生存期短暂的线程时.
1. 线程池在系统启动时就创建大量空闲线程, 程序将一个Runnable对象或Callable对象传给线程池, 线程池就会启动一个线程来执行它们的run()或call()方法, 执行结束线程不死, 而是返回线程池中称为空闲状态等待执行下一个Runnable对象的run()或call()方法.
2. 线程池的最大县城参数可以控制系统中并发线程数不超过此数.
8.1 Java 5 实现的线程池
0. Executors工厂类包括几个静态工厂方法创建线程池:
- newCachedThreadPool(): 创建一个有缓存功能的线程池, 系统根据需要创建线程, 这些线程将会被缓存在线程池中.
- newFixedThreadPool(int nThreads): 创建一个可重用的, 具有固定线程数的线程池.
- newSingleThreadExecutor(): 创建一个只有一个线程的线程池, 相当于使用newFixedThreadPool()方法时传入1.
- newScheduledThreadPool(int corePoolSize): 创建具有指定线程数的线程池, 它可以在指定延迟后执行线程任务. corePoolSize指池中所保存的线程数(即使线程是空闲的也被保存在线程池内).
- newSingleThreadScheduledExecutor(): 创建只有一个线程的线程池, 它可以在指定延迟后执行线程任务.
# 前三个方法返回ExecutorService对象; 后两个返回ScheduledExecutorService线程池(它是ExecutorService的子类), 它可以在指定延迟后或周期性地执行线程任务.
1. ExecutorService的三个方法:
- Future<?> submit(Runnable task): 将一个Runnable对象提交给指定线程池, 线程池将在有空闲线程时执行Runnable对象代表的任务. Future对象代表Runnable任务的返回值(run()方法没有返回值, 所以Future此时返回null), 还可以调用Future的isDone(), isCancelled()方法获得Runnable对象的执行状态.
- <T> Future<T> submit(Runnable task, T result): result显示指定线程执行结束后的返回值, 所以Future对象将在run()方法执行结束后返回result.
- <T> Future<T> submit(Callable<T> task): Future代表Callable对象里的call()方法的返回值.
2. ScheduledExecutorService的三个方法:
- ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit): 指定callable任务将在delay延迟后执行.
- ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit): 指定command任务将在delay延迟后执行.
- ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit): 在initialDelay, initialDelay+period, initialDelay+2*period....处重复执行.
- ScheduledFuture<?> scheduleWithFixeDelay(Runnable command, long initialDelay, long delay, TimeUnit unit): 在initialDelay处开始重复执行, 且每一次执行中止和下一次执行开始之间间隔delay.
3. 用完一个线程池后, 应该将其关闭.
- shutdown(): 线程池不再接受新任务, 但已提交任务会执行完成(所有任务执行完后, 池中所有线程死亡).
- shutdownNow(): 该方法试图停止所有正在执行的活动任务, 暂停处理正在等待的任务, 并返回等待执行的任务列表.
4. 例子.
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
class MyThread implements Runnable
{
public void run()
{
for (int i = 0; i < 100; i++)
{
System.out.println(Thread.currentThread().getName() + "的i值为: " + i);
//System.out.println(getName() + "的i值为: " + i);
}
}
}
public class ThreadPoolTest
{
public static void main(String[] args) throws Exception
{
ExecutorService pool = Executors.newFixedThreadPool(6);
pool.submit(new MyThread());
pool.submit(new MyThread());
pool.shutdown();
}
}
8.2 Java 7 新增的ForkJoinPool
0. 引入ForkJoinPool是为了充分利用多cpu优势. ForkJoinPool把一个任务分成数个小任务给各cpu并行计算, 计算结果再合成一个任务.
1. ForkJoinPool的两个构造器:
- ForkJoinPool(int parallelism): 创建一个包含parallelism个并行线程的ForkJoinPool.
- ForkJoinPool(): 以Runtime.availableProcessors()方法的返回值作为parallelism参数来创建ForkJoinPool.
9. 线程相关类
9.1 ThreadLocal类
0.
java.lang.ThreadLocal<T>.
1. ThreadLocal(即thread local variable, 线程局部变量), 通过把数据放在ThreadLocal中, 每个使用该变量的线程都将得到一个ThreadLocal的副本, 从而避免并发访问的线程安全问题.
2. ThreadLocal的单个public方法:
- T get(): 返回此线程局部变量中当前线程副本中的值.
- void remove(): 删除此线程局部变量中当前线程的值.
- void set(T value): 设置此线程局部变量中当前线程副本中的值.
3. 例子.
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 account, String name)
{
super(name);
this.account = account;
}
public void run()
{
for (int i = 0; i < 10; i++)
{
if ( i == 6)
{
account.setName(getName());
}
System.out.println(account.getName() + "'s i is: " + i);
}
}
}
public class ThreadLocalTest
{
public static void main(String[] args)
{
Account at = new Account("initName");
new MyTest(at, "Thread-A").start();
new MyTest(at, "Thread-B").start();
}
}
9.2 包装线程不安全的集合
0. 像ArrayList, LinkedList, HashSet, TreeSet, HashMap, TreeMap等集合都是线程不安全的. 可以使用Collections提供的静态方法把这些集合包装成线程安全的集合:
- <T> Collection<T> synchronizedCollection(Collection<T> c): 返回指定collection对应的线程安全的collection.
- static <T> List<T> synchronizedList(List<T> list): 返回指定List对象对应的线程安全List对象.
- static <K, V> Map<K, V> synchronizedMap(Map<K, V> m): 返回指定Map对象对应的线程安全Map对象.
- static <T> Set<T> synchronizedSet(Set<T> s): 返回指定Set对象对应的线程安全Set对象.
- static <K, V> SortedMap<K, V> synchronizedSortedMap(SortedMap<K, V> m): 返回指定SortedMap对象对应的线程安全SortedMap对象.
- static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s): 返回指定SortedSet对象对应的线程安全SortedSet对象.
1. 如果需要把某个集合包装成线程安全的集合, 则应该在创建之后立即包装.
9.3 线程安全的集合
0. java.util.concurrent包提供了大量支持高效并发访问的集合接口和实现类.
1. 以Concurrent开头的集合类: ConcurrentHashMap, ConcurrentSkipListMap, ConcurrentSkipListSet等.
- 支持多个线程并发写入访问(安全), 但读取操作不必锁定.
- 采用复杂算法保证了永远不会锁住整个集合.
2. CopyOnWrite开头的集合类: CopyOnWriteArrayList, CopyOnWriteArraySet等.
- CopyOnWriteArraySet的底层封装了CopyOnWriteArrayList, 所以实现机制类似.
- 线程对CopyOnWriteArrayList集合执行读取操作时, 线程直接读取集合本身, 无需加锁与阻塞. 执行写操作时, 该集合在底层复制一份新的数组, 对新数组进行写入操作.
- 对CopyOnWriteArrayList写入时频繁复制数组, 性能差, 但读取时性能好, 所以CopyOnWriteArrayList适合用在读取操作远远大于写入操作的场景中, 如缓存.