一、什么是线程?
线程是操作系统能够进行运算调度的最小单位,线程是进程中的一个执行单元,被包含在进程之中,是进程中的实际运作单位。一个进程(进程就是应用程序比如qq、微信等)中可以有多个线程,且至少有一个线程。
二、java中线程的三种创建方式
- 继承Thread,重写run方法
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i <20 ; i++) {
System.out.println("线程启动方法执行:"+i);
}
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.run();// 直接调用run,就相当于调了一个普通方法,那么代码会按顺序执行,走完run方法再走下面的代码
// start()是开辟了一个线程,开辟后下面的代码会继续执行
//也就相当于有两个线程在同时执行,一个刚开辟的这个,一个main方法线程
myThread.start();
for (int i = 0; i < 200; i++) {
System.out.println("main方法执行:"+i);
}
}
}
- 实现Runnable接口,重写run方法 (此创建方式可实现多个线程资源共享)
class MyRunnableImpl implements Runnable{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("MyRunnableImpl的run方法执行了");
}
}
public static void main(String[] args) {
// 先创建一个runnable接口实现类对象
MyRunnableImpl m1 = new MyRunnableImpl();
//然后new一个Thread类并将runnable接口实现类对象传入,开启线程
new Thread(m1,"线程1").start();
// 也可以使用Lambda表达式直接创建线程,省去编写Runnable实现类的步骤
new Thread(()->{
for (int i = 0; i < 20; i++) {
System.out.println("Lambda表达式的run方法执行了");
}
},"线程2").start();
}
}
- 实现Callable接口,重写call方法
// 3. 实现callable接口,重写call方法
// 此种方式的好处是: a.可以获取返回值 b.可以抛出异常
class MyCallable implements Callable<Boolean>{
@Override
public Boolean call(){
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+":MyCallable的call方法执行了");
}
return true;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable myCallable = new MyCallable();
// 创建执服务
ExecutorService ser = Executors.newFixedThreadPool(3);
// 提交执行(也就是启动线程)
Future<Boolean> f1 = ser.submit(myCallable);
// 获取线程call方法返回结果
Boolean b1 = f1.get();
System.out.println(b1);
// 关闭服务
ser.shutdown();
}
}
三、线程的几种状态
线程状态图:
由上图可以看到,线程共经历的大致有6种状态:
- 初始(NEW): 初始创建状态,也就是线程刚被new出来,还没调用start()方法开启线程
- 运行(RUNNABLE):java中将就绪状态(ready)和运行中状态(running)统称为RUNNABLE运行状态,线程调用start()方法开启线程后,线程进入就绪状态,此时等待CPU进行调度,线程被CPU选中获取到CPU的使用权后进入运行状态。
- 阻塞(BLOCKED):阻塞状态,表示线程被锁住了,也就是同步锁那里,需要等待锁被释放后才能继续运行。
- 等待(WAITING):当调用wait方法,该线程进入等待状态
- 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。比如sleep方法。
- 终止(TERMINATED):线程执行完毕,终止。
BLOCKED、WAITING、TIMED_WAITING其实也统称为阻塞状态,进入这些状态的线程,当解除阻塞状态后,会重新进入就绪状态等待CPU调度,然后获得CPU时间片后进入运行中状态。
以下是关于线程调度的几个常用的API,比如休眠、礼让、优先级等:
public class ThreadStatus {
public static void main(String[] args) throws Exception{
//sleep();
yield();
}
private static boolean flag = true;
// 停止线程,不推荐使用jdk的stop()方法,而是建议建立一个flag标识,到达某一条件自动终止线程
private static void stopThread() {
flag = false;
}
private void stop(){
// 线程停止
Thread t1 = new Thread(() -> {
while (flag) {
System.out.println(Thread.currentThread().getName() + "执行中...");
}
}, "线程1");
t1.start();
System.out.println(flag);
for (int i = 0; i < 1000; i++) {
System.out.println("main方法执行" + i);
if (i == 900) {
System.out.println("线程准备停止...");
stopThread();
}
}
}
private static void sleep(){
// 线程休眠 sleep,sleep也就是让线程进入阻塞状态,但是sleep并不会释放锁,也就是还占着这把锁,其他同步线程不能执行
new Thread(()->{
int num = 10;
while (true){
try {
Thread.sleep(1000);
System.out.println(num--);
if(num<=0){
break;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"线程2").start();
}
// 线程礼让,让正在运行状态的线程暂停,但不阻塞,会让线程从运行状态转为就绪状态,然后CPU重新调度。
// 礼让不一定成功,因为CPU调度是随机性的
private static void yield(){
Runnable r1 = ()->{
System.out.println(Thread.currentThread().getName()+"线程开始执行");
Thread.yield();// 线程礼让
System.out.println(Thread.currentThread().getName()+"线程执行结束");
};
new Thread(r1,"a").start();
new Thread(r1,"b").start();
}
}
// join 线程强制执行,想象为插队
class join{
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
System.out.println("线程执行中"+i);
}
});
thread.start();
for (int i = 0; i < 100 ; i++) {
if(i==50){
thread.join();// thread线程插队,此时只有当thread执行完毕后,main线程才会继续执行
}
System.out.println("main线程执行中"+i);
}
}
}
// 线程优先级 1-10 , 越大表示优先级越高,获得cpu调度的概率就越大,但并不是低就一定最后调用,这只是个概率高低的问题。
class Priority{
public static void main(String[] args) {
System.out.println("main线程优先级"+Thread.currentThread().getPriority());
Runnable r1 = ()->{
System.out.println(Thread.currentThread().getName()+"优先级:"+Thread.currentThread().getPriority());
};
Thread t1 = new Thread(r1,"线程1");
Thread t2 = new Thread(r1,"线程2");
Thread t3 = new Thread(r1,"线程3");
Thread t4 = new Thread(r1,"线程4");
Thread t5 = new Thread(r1,"线程5");
t1.setPriority(1);
t1.start();
t2.setPriority(5);
t2.start();
t5.setPriority(10);
t5.start();
// 不设置优先级,默认就是5
t3.start();
t4.start();
}
}
// 守护线程:线程分为 用户线程和守护线程,
// 用户线程就是我们写的线程,虚拟机必须确保我们的用户线程执行完毕
// 守护线程:虚拟机不用等待守护线程执行完毕,如垃圾回收GC, 后台操作日志记录、监控内存等
class Daemon{
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
System.out.println("用户线程执行..." + i);
}
});
Thread t2 = new Thread(() -> {
while (true){ // 这里设置为true表示一直执行
System.out.println("守护线程执行...");
}
});
t2.setDaemon(true);// true表示设置该线程为守护线程,默认false
t1.start();
t2.start();
}
}
四、静态代理
代理模式是23中设计模式其中之一,可以在不修改被代理对象的基础上,通过扩展代理类,进行一些功能的附加与增强。值得注意的是,代理类和被代理类应该共同实现一个接口,或者是共同继承某个类。
在线程的创建中,通过创建Runnable接口实现类,并让线程Thread类代理该实现类来创建线程,此种方式就用到了代理模式。因为我们创建的类实现了Runnable接口,Thread同样也实现了Runnable接口。
Spring中的AOP用到的就是动态代理,动态代理和静态代理都是代理模式的实现,只是区别在于动态代理是根据反射动态的创建代理类,静态代理则是已经写好的代理类。这里重点先介绍静态代理,后面有机会再详细介绍动态代理的实现。
package com.hhl.demo1;
/**
* 静态代理:就是下面的代码,简单来说就是,这些代码是我们写死的,所以叫静态代理
* (比如说婚庆公司结婚前就只有布置现场吗?我还想让他帮做点其他的,比如我定制自己的婚礼呢?
* 难道我的需求每变一次都要重新写一个代理类(婚庆公司类)吗?当然不用,不过这就要用到后面的动态代理了)
*
*/
public class StaticProxy {
public static void main(String[] args) {
//普通的结婚,不用代理
You you = new You();
you.marryMethod();
System.out.println("==========================");
//使用婚庆公司结婚,用代理模式
WenddingCorp wenddingCorp = new WenddingCorp(you);
wenddingCorp.marryMethod();
// 经过结果分析可以发现,目的都是结婚,普通的结婚就是单纯的结婚
// 而使用婚庆公司的话,你也可以结婚,并且在结婚前后婚庆公司还可以帮你干很多事,这也就是代理模式
}
}
// 接口,结婚
interface Marry{
void marryMethod();
}
// 人,实现了结婚接口,那么人就可以结婚了
class You implements Marry{
@Override
public void marryMethod() {
System.out.println("我要结婚了,我很开心");
}
}
// 婚庆公司,也实现了结婚接口,那婚庆公司也可以结婚
class WenddingCorp implements Marry{
// 重点在这里,婚庆公司的类要传入一个结婚Marry接口
private Marry marry;
// 写有参构造
public WenddingCorp(Marry marry) {
this.marry = marry;
}
@Override
public void marryMethod() {
// target结婚方法执行之前要调用的方法
before();
// 在婚庆公司类重写的结婚方法中,调用传入的target的结婚方法,这样也就相当于婚庆公司的结婚方法,代理了人的结婚方法
marry.marryMethod();
// target结婚方法之后要调用的方法(这样其实就相当于对人结婚的方法做了增强)
after();
}
private void before() {
System.out.println("结婚之前,布置现场");
}
private void after() {
System.out.println("结婚之后,付尾款");
}
}
五、线程同步
当有多个线程要同时访问一个变量或者对象时,若只是读还好,但涉及到写操作时,就会出现变量值或对象的状态混乱,从而发生一些异常。
这种情况在生活中很常见,比如电影院卖票,票的数量是一定的,但是很多人都在买这个电影院的票,每个人就是一个线程,若不对这些线程进行同步管理,那很可能就会出现100张票被多于100个人买到这种问题的发生。
再比如,一个银行账户,账户的钱是一定的有100,此账户同时被两个人在操作,A要取100,B要取50,此时若不对AB两个线程进行同步管理,那很可能出现A、B同时取到钱这种问题的发生。
java中对于多线程同步解决的办法就是:加锁。就是多个线程在操作同一变量或者对象时,对此变量或对象加一把锁,此时其他所有线程不能访问,必须等该线程释放锁以后,才能继续访问操作这一变量或对象。synchronized 关键字就是java中的锁,这个关键字可用来直接修饰方法,或修饰代码块。
以下是通过时间Runnable接口创建多线程,synchronized直接修饰了方法,那么锁对象就是this,也就是该Runnable实现类,而多个线程创建时都是使用的这一个实现类,也就是说锁对象是唯一的,所以synchronized起到了作用,线程是安全的
/**
* synchronized 是关键字,是一种同步锁。
* 1. 当修饰方法时,synchronized 里的这把锁就是该方法所在的类对象,也就是this
* 2. 当修饰代码块时, synchronized(lock){...} ,这里的lock是一个锁对象,该对象可以是任意类型,但是多个对象要使用同一把锁才能起到效果
* <p>
* 注意: 其实两种修饰的方式都一样,重点是这把锁,一定要是同一把锁,才能起到同步的效果。
* 就是多个线程运行时,这把锁一定要是同一个对象,若是不同的对象那根本就起不到同步的效果。
*/
public class ThreadSynchronized {
public static void main(String[] args) {
BuyTicket b1 = new BuyTicket();
new Thread(b1, "用户A").start();
new Thread(b1, "用户B").start();
new Thread(b1, "用户C").start();
}
}
class BuyTicket implements Runnable {
private int tickets = 10;
boolean flag = true;
Object obj = new Object();
@Override
public void run() {
while (flag) {
try {
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// synchronized 可以直接锁方法,那锁的对象其实就是this, 这个方法所在的类就是那把锁
// 因为我们这里的线程是实现Runnable方法,多个线程操作的都是一个BuyTicket对象,所以这把锁是该类不会有问题
private synchronized void buy() throws InterruptedException {
if (tickets <= 0) {
flag = false;
return;
}
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + "买到了第" + tickets-- + "张票");
}
}
以下是通过继承Thread类来创建线程,因为不同于实现Runnable的方式,此种方式需要一个外部的对象来当这把锁,且必须多个线程的这把锁是唯一的。虽然锁对象没有要求,只需多个线程用同一个即可,但一般来说锁对象就是那个需要共享的变量或对象。
public class UnsafeBank {
public static void main(String[] args) {
Account account = new Account(100, "结婚基金");
// 多个线程用的是同一个Account对象,所以锁是同一把,那么就可以实现同步
Drawing you = new Drawing(account, 50, "你");
Drawing girlFriend = new Drawing(account, 100, "你老婆");
you.start();
girlFriend.start();
}
}
class Account {
int money;
String name;
public Account(int money, String name) {
this.money = money;
this.name = name;
}
}
// 注意 这里是继承的Thread
class Drawing extends Thread {
Account account;
int drawingMoney;
int nowMoney;
public Drawing(Account account, int drawingMoney, String name) {
super(name);
this.account = account;
this.drawingMoney = drawingMoney;
}
@Override
public void run() {
// 注意:由于这里的线程是继承Thread类,所以每一个线程就是一个单独的线程,但是多个线程操作的是一个Account对象,所以这里的锁是account对象也不会有问题
// 但是,如果将synchronized写到方法上,就表示锁对象是该类,而每个线程又是单独的类,(因为是通过继承Thread创建的线程),所以就不会起到同步的作用了
synchronized (account) {
if (account.money - drawingMoney < 0) {
System.out.println(Thread.currentThread().getName() + "来取钱,要取"+drawingMoney+",钱不够了,取不了那么多了");
return;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.money = account.money - drawingMoney;
nowMoney = nowMoney + drawingMoney;
System.out.println(account.name + "余额为:" + account.money);
System.out.println(this.getName() + "手里目前有:" + nowMoney);
}
}
}
jdk5推出了JUC包,也就是java.util.concurrent包,此包专门用来解决多线程问题,里面提供了一个lock锁,这个锁和synchronized关键字的作用一致,都是锁,用来解决多线程同步问题的,两者相比,synchronized是一个内置锁,就是把锁封装了,自动帮我们加锁和释放锁,这个过程我们看不到;lock锁是一个显示锁,需要显示的加锁和释放锁,相对于synchronized来说,
lock锁对于锁的操作具有更强的可操作性、可控制性以及提供可中断操作和超时获取锁等机制。
public class LockThread {
public static void main(String[] args) {
BuyTicket2 r1 = new BuyTicket2();
new Thread(r1, "用户A").start();
new Thread(r1, "用户B").start();
new Thread(r1, "用户C").start();
}
}
class BuyTicket2 implements Runnable {
private int tickets = 10;
boolean flag = true;
// 使用lock锁
Lock lock = new ReentrantLock();
@Override
public void run() {
while (flag) {
try {
// 加锁
lock.lock();
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
// 释放锁
lock.unlock();
}
}
}
private void buy() throws InterruptedException {
if (tickets <= 0) {
flag = false;
return;
}
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + "买到了第" + tickets-- + "张票");
}
}
六、线程通信
线程通信,从字面意思来看就是多个线程之间需要互相通信。也就是说线程与线程之间并不是独立的个体,线程与线程之间有时是需要互相通信和协作的。其中最典型的就是生产者和消费者问题。
- 仓库中只能放一件产品,生产者生产,消费者消费。
- 仓库没有产品时,生产者需要开始生成,而消费者来消费必须等待
- 仓库有产品,生产者不能生产需要等待,消费者此时来消费,消费后生产者才能继续生产。
- 生产者和消费者是两个不同的线程,但是又因为某些条件必须互相约束,此时就要用到线程通信机制来解决,常用的也就是线程中的等待/唤醒机制 wait()/notify()
生产者消费者问题的解决办法,这里列举两个。
解决方法1:管程法,利用缓冲区解决问题。此种方法主要就是在生产者和消费者之间建立一个缓冲区,生产者与消费者通过缓冲区进行交流和通信。
public class ThreadComm {
public static void main(String[] args) {
SynContainer synContainer = new SynContainer();
new Producer(synContainer).start();
new Consumer(synContainer).start();
}
}
//生产者(KFC)
class Producer extends Thread{
// 生产者和消费者都是根据这个容器来建立连接的,所以都需要这个参数
SynContainer container;
public Producer(SynContainer container){
this.container=container;
}
@Override
public void run() {
// 生产者只管生产,要生产100只鸡,但是容器只有10,所以容器满了后就会等待消费者消费
for (int i = 0; i < 100; i++) {
container.push(new Chicken(i));
System.out.println("KFC生产了第"+i+"只鸡");
}
}
}
//消费者(顾客)
class Consumer extends Thread{
// 生产者和消费者都是根据这个容器来建立连接的,所以都需要这个参数
SynContainer container;
public Consumer(SynContainer container){
this.container=container;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("顾客消费了第"+container.pop().id+"只鸡");
}
}
}
// 产品(炸鸡)
class Chicken{
int id;
public Chicken(int id) {
this.id = id;
}
}
//缓存区,管程法 就是根据生产者消费者之间建立的这个缓冲区,来相互制约
class SynContainer{
//创建一个容器(放炸鸡的容器)
Chicken[] chickens = new Chicken[10];
// 容器计数器,记录容器里目前有几个产品(也就是记录当前容器里几只鸡)
int count=0;
// 生产者放入产品容器(KFC做好炸鸡后,放到容器里)
public synchronized void push(Chicken chicken){
// 容器满了
if(count==chickens.length){
try {
this.wait();// 需要等待消费者消费
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 容器没满,那就放入产品
chickens[count]=chicken;
count++;
//容器里有产品了,通知消费者进行消费
this.notifyAll();
}
//消费者消费产品
public synchronized Chicken pop(){
// count==0 表示目前没有产品,不能消费,
if(count==0){
try {
this.wait();// 需要等待生产者生产
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 容器有产品,那就消费
count--;
Chicken chicken = chickens[count];
//消费过后,通知生产者进行生成
this.notifyAll();
return chicken;
}
}
解决方法2:信号灯法,通过标志位解决,也就是一个布尔值flag。
/**
* 生产者消费者问题解决方法2:
* 信号灯法:通过标志位解决,也就是一个布尔值flag
*/
public class ThreadCommTwo {
public static void main(String[] args) {
TVShow tvShow = new TVShow();
new Player(tvShow).start();
new Watcher(tvShow).start();
}
}
// 生产者 (演员,录节目)
class Player extends Thread {
TVShow tvShow;
public Player(TVShow tvShow){
this.tvShow=tvShow;
}
@Override
public void run() {
for (int i = 0; i <20 ; i++) {
if(i%2==0){
this.tvShow.play("快乐大本营");
}else{
this.tvShow.play("天天向上");
}
}
}
}
// 消费者 (观众,看节目)
class Watcher extends Thread {
TVShow tvShow;
public Watcher(TVShow tvShow){
this.tvShow=tvShow;
}
@Override
public void run() {
for (int i = 0; i <20 ; i++) {
tvShow.watch();
}
}
}
// 产品 (节目)
class TVShow {
String voice;// 产品(节目)
boolean flag = true; // 标志位,true表示现在没节目,需要演员录制观众等待。反之false表示现在有节目,演员休息观众观看
// 录节目
public synchronized void play(String voice) {
if(!flag){// 有节目,演员等待 (等观众看)
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.voice = voice;
this.flag = !this.flag;
System.out.println("演员录制了:" + voice);
this.notifyAll();//唤醒消费者(通知观众观看)
}
// 看节目
public synchronized void watch(){
if (flag){// 节目没有,观众等待 (等演员录制)
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("观众观看了:"+voice);
this.flag=!this.flag;
this.notifyAll();// 观看完了,唤醒演员,通知演员录节目
}
}