线程的生命周期
当一个线程被创建直到这个线程被销毁,中间的过程就是这个线程的生命周期。
一个完整的生命周期通信要经历如下五个步骤:
- 新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态。
- 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时他已经具备了运行的条件,只是没分配到CPU资源
- 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能
- 阻塞:在某种特殊情况下,被人挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态
- 死亡:线程完成了它的全部工作或线程被提前强制性中止或出现异常导致结束
线程安全问题
上篇文章中卖票问题有一个小bug,就是三个窗口会同时卖第一张票,也就是第一张票被卖了三次,一共卖了102张票。
问题的原因:当某个线程在操作车票的过程中,尚未操作完,其他线程参与进来,也操作车票
解决的思路:当一个线程a在操作车票的时候,其他线程不能参与进来,直到线程a操作完后其他线程才可以操作。也就是当一个线程操作时,就把车票给锁起来。–也就是同步锁
同步锁有两种解决方式:一是同步代码块,二是同步方法。
同步代码块
----------
synchronized(同步监视器){
// 需要同步的代码
}
注解:操作共享数据的代码,即为需要被同步的代码
共享数据:多个线程共同操作的变量,比如:static就是共享数据
同步监视器:锁,任何一个类的对象,都可以充当锁
例子:
Runnable接口实现类:
public class WindowTest {
public static void main(String[] args) {
Window win = new Window();
Thread t1 = new Thread(win);
Thread t2 = new Thread(win);
Thread t3 = new Thread(win);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class Window implements Runnable{
private int ticket = 100;
Object obj = new Object();
@Override
public void run() {
while(true){
synchronized(obj){
if(ticket > 0){
System.out.println(Thread.currentThread().getName() + ":卖票,票号为" + ticket);
ticket --;
}else{
break;
}
}
}
}
}
--------------------
Thread子类
public class WindowTest {
public static void main(String[] args) {
Window win1 = new Window("窗口一");
Window win2 = new Window("窗口二");
Window win3 = new Window("窗口三");
win1.start();
win2.start();
win3.start();
}
}
class Window extends Thread{
private static int ticket = 100;
private static Object obj = new Object();
@Override
public void run() {
while(true){
synchronized(obj){
if(ticket > 0){
System.out.println(getName() + ":卖票,票号为" + ticket);
ticket --;
}else{
break;
}
}
}
}
public Window(String name){
super(name);
}
}
同步方法
---------
public synchronized void show() {
}
注解:如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。
例子:
Runnable接口实现类:
public class WindowTest {
public static void main(String[] args) {
Window win = new Window();
Thread t1 = new Thread(win);
Thread t2 = new Thread(win);
Thread t3 = new Thread(win);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class Window implements Runnable{
private int ticket = 100;
Object obj = new Object();
@Override
public void run() {
while(true){
show();
}
}
public static synchronized void show(){
if(ticket > 0){
System.out.println(Thread.currentThread().getName() + ":卖票,票号为" + ticket);
ticket --;
}
}
}
--------------------
Thread子类
public class WindowTest {
public static void main(String[] args) {
Window win1 = new Window("窗口一");
Window win2 = new Window("窗口二");
Window win3 = new Window("窗口三");
win1.start();
win2.start();
win3.start();
}
}
class Window extends Thread{
private static int ticket = 100;
@Override
public void run() {
while(true){
show();
}
}
public static synchronized void show(){
if(ticket > 0){
System.out.println(Thread.currentThread().getName() + ":卖票,票号为" + ticket);
ticket --;
}
}
public Window(String name){
super(name);
}
}
注意:同步方法依旧涉及到同步监视器,但是不需要显式声明
静态同步方法默认是当前类本身
非静态同步方法默认是this
死锁
什么是死锁
- 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
- 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
解决方法
- 专门的算法,原则
- 尽量避免同步资源的定义
- 尽量避免嵌套同步
死锁的例子:
public class BankThread {
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
new Thread() {
@Override
public void run() {
synchronized (s1) {
s1.append("a");
s2.append("1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(s2){
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
new Thread(new Runnable(){
@Override
public void run() {
synchronized(s2){
s1.append("c");
s2.append("3");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(s1){
s1.append("d");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}
}).start();
}
}
这个例子里面有两个线程,第一个线程先上s1的锁,再上s2的锁;第二个线程相反,等第一个线程执行完第一个锁打算拿第二个锁时,发现第二个锁在第二个线程中;第二个线程同理,就进入了死锁状态。
大概就是:
A:把你手里的锁给我
B:凭什么,先把你手里的锁给我
A:你给不给
B:你先给我我就给你
A:。。。。
线程的同步
- 从JDK5.0开始,Java就提供了更强大的线程同步机制–通过显式定义同步锁来实现同步。同步锁使用Lock对象充当。
- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
- ReentrantLock类实现了Lock,他拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常见的是ReentranLock,可以显式加锁、释放锁。
import java.util.concurrent.locks.ReentrantLock;
public class LockTest {
public static void main(String[] args) {
Window1 win = new Window1();
Thread t1 = new Thread(win);
Thread t2 = new Thread(win);
Thread t3 = new Thread(win);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class Window1 implements Runnable{
private static int ticket = 100;
// 1.实例化ReentrantLock
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while(true){
try{
// 2.调用上锁方法
lock.lock();
if(ticket > 0){
System.out.println(Thread.currentThread().getName() + ":卖票,票号为" + ticket);
ticket --;
}else{
break;
}
}finally{
// 3.调用解锁方法
lock.unlock();
}
}
}
}
核心:synchronized和lock的异同
同:二者用来解决线程的安全问题
异:synchronized自动的释放同步监视器,lock需要手动释放;lock是显式锁,synchronized是隐式锁;lock只有代码块锁,synchronized有代码块锁和方法锁
使用lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性。
线程的通信
线程的通信涉及到三个方法:
wait():让线程阻塞,释放锁
notify():唤醒阻塞的线程中优先级最高的线程
notifyAll():唤醒全部线程
注意:这三个方法必须使用在同步代码块或同步方法中,必须由同步监视器调用。否则会报IllegalMonitorStateException
异常。
这三个方法在object类中。
public class CommunicationTest {
public static void main(String[] args) {
Number num = new Number();
Thread t1 = new Thread(num);
Thread t2 = new Thread(num);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
class Number implements Runnable {
private int number = 1;
@Override
public void run() {
while (true) {
synchronized (this) {
// 唤醒阻塞的线程,避免线程全部阻塞
// 线程1运行到wait阻塞后,线程2运行到这里会把线程1唤醒,周而复始
notify();
if (number <= 100) {
System.out.println(Thread.currentThread().getName() + ":" + number);
number++;
try {
// 主动让线程阻塞来做到主动切换线程
// 记得唤醒线程,否则线程会全部阻塞
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
break;
}
}
}
}
}
问题:sleep()和wait()的异同
调用方法后,都可以让线程进入阻塞状态
两个方法声明的位置不同,前者在thread类中,后者在object类中
sleep()可以在任何需要的场景下使用,wait()必须在同步代码块或同步方法内
sleep()不会释放同步监视器,wait()会释放同步监视器
生产者/消费者问题
生产者将产品交给店员,而消费者从店员处取走产品,店员一次只能持有固定数量的产品,如果生产者试图生产更多的产品,店员就会叫停生产者,等有空位了再让生产者生产;如果店内没有商品了,就会告诉消费者等一下,如果有产品了再通知消费者取走产品。
这里就有两个场景
- 生产者比消费者快,消费者会漏掉一些商品没有取到
- 消费者比生产者快,消费者会取到相同的数据
public class ProductTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Producer p1 = new Producer(clerk);
p1.setName("生产者1");
Consumer c1 = new Consumer(clerk);
c1.setName("消费者1");
p1.start();
c1.start();
}
}
class Clerk {
private int productCount = 0;
// 生产产品
public void produceProcuct() {
if (productCount < 20) {
productCount++;
System.out.println(Thread.currentThread().getName() + ":开始生产第" + productCount + "个产品");
notify();
} else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 消费产品
public void consumerProcuct() {
if (productCount > 0) {
System.out.println(Thread.currentThread().getName() + ":开始消费第" + productCount + "个产品");
productCount--;
notify();
} else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Producer extends Thread{
private Clerk clerk;
public Producer(Clerk clerk){
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(getName()+"开始生产产品...");
while(true){
synchronized(clerk){
clerk.produceProcuct();
}
}
}
}
class Consumer extends Thread{
private Clerk clerk;
public Consumer(Clerk clerk){
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(getName()+"开始消费产品...");
while(true){
synchronized(clerk){
clerk.consumerProcuct();
}
}
}
}
callable接口
-
callable接口的run方法可以有返回值
-
方法可以抛出异常
-
支持泛型的返回值
-
需要借助FutureTask类,比如获取返回结果
- FutureTask类可以对具体Runnable、callable任务的执行结果进行取消、查询是否完成、获取结果等。
- FutrueTask是Futrue接口的唯一实现类
- FutrueTask即可以作为Runnable被线程执行,又可以作为Futrue得到callable的返回值
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThreadNew {
public static void main(final String[] args) {
final NumThread num = new NumThread();
final FutureTask f = new FutureTask(num);
new Thread(f).start();
try {
// get的返回值就是FutureTask构造器参数callable实现类重写的call()的返回值。
final Object sum = f.get();
System.out.println("总和为"+sum);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
class NumThread implements Callable{
@Override
public Object call() throws Exception {
int sum = 0;
for(int i = 1;i <= 100;i ++){
if(i % 2 == 0){
System.out.println(i);
sum += 1;
}
}
return sum;
}
}
线程池
如果需要经常创建和销毁线程,使用量特别大,比如高并发情况下的线程,就可以使用线程池。我们可以提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁,实现重复使用。
- 好处:
- 提高响应速度
- 降低资源消耗
- 便于线程管理
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
public class ThreadPool{
public static void main(String[] args) {
// 创建一个包含指定数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
ThreadPoolExecutor t = (ThreadPoolExecutor) service;
// 核心池的大小
t.setCorePoolSize(5);
System.out.println(t.getCorePoolSize());
// 最大线程数
t.setMaximumPoolSize(15);
System.out.println(t.getMaximumPoolSize());
// 线程自动关闭时间
// t.setKeepAliveTime();
// t.getKeepAliveTime()
service.execute(new NumberThread()); // 适合使用于Runnable
service.execute(new NumberThread1());
// service.submit(task); // 适合使用于callable
// 关闭线程池
service.shutdown();
}
}
class NumberThread implements Runnable{
@Override
public void run() {
for(int i = 0;i <= 100;i ++){
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}
class NumberThread1 implements Runnable{
@Override
public void run() {
for(int i = 0;i <= 100;i ++){
if(i % 2 != 0){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}