一、Java多线程常用的两种实现方法
1、 继承Thread类
-
子类继承Thread类具备多线程能力
-
启动线程:子类对象.start()
不建议使用:避免OOP单继承局限性
2、 实现Runnable接口
- 实现接口Runnable接口具有多线程能力
- 启动线程:传入目标对象+Thread对象.start()
new Thread(new ClassImplementRunnable()).start()
推荐使用Runnable:避免单继承局限性,灵活方便,方便同一个对象被多个线程使用(静态代理)。
一份资源,多个代理
//一份资源
StartThread station = new StartThread();
//多个代理
new Thread(station,"user_a").start();
new Thread(station,"user_b").start();
new Thread(station,"user_c").start();
例子-龟兔赛跑
//模拟龟兔赛跑
public class Race implements Runnable {
private static String winner;
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
System.out.println(Thread.currentThread().getName() + "跑了" + i + "步");
//让兔子睡觉
if(Thread.currentThread().getName().equals("兔子") && i==40){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (getWinner(i)) {
break;
}
}
}
//判断是否完成比赛
public boolean getWinner(int step) {
//判断是否有胜利者
if (winner != null) {
return true;
} else {
if (step >= 100) {
winner = Thread.currentThread().getName();
System.out.println("winner is " + winner);
return true;
} else {
return false;
}
}
}
public static void main(String[] args) {
Race race = new Race();
new Thread(race,"乌龟").start();
new Thread(race,"兔子").start();
}
}
二、静态代理
- 真实对象和代理对象都要实现同一个接口;
- 代理对象要代理真实角色;
优点:
- 代理对象可以做真实对象做不了的事情(before,after)。
- 真实对象专注做自己的事情。
三、Lamda表达式
理解Function Interface(函数式接口)是学习Java8 lamda表达式的关键坐在。
函数式接口定义:
- 任何接口,如果只包含唯一一个抽象方法,那么它就是一个函数式接口。
public interface Runnable(){
public abstract void run();
}
- 对于函数式接口,我们可以通过lambda表达式创建该接口的对象。
Lamda表达式的演化:
//推导lamda表达式
public class TestLamda1 {
//3.静态内部类
static class Like2 implements ILike{
@Override
public void lamda() {
System.out.println("i like lamda2");
}
}
public static void main(String[] args) {
ILike like = new Like();
like.lamda();
like = new Like2();
like.lamda();
//4.局部内部类
class Like3 implements ILike{
@Override
public void lamda() {
System.out.println("i like lamda3");
}
}
like = new Like3();
like.lamda();
//5.匿名内部类,没有类的名称,必须借助接口或者父类
like = new ILike() {
@Override
public void lamda() {
System.out.println("i like lamda4");
}
};
like.lamda();
//6.用lamda简化
like = () -> {
System.out.println("i like lamda5");
};
like.lamda();
}
}
//1.定义一个函数式接口
interface ILike{
void lamda();
}
//2.实现类(常规用法)
class Like implements ILike{
@Override
public void lamda() {
System.out.println("i like lamda");
}
}
lambda表达式简化:
- lambda表达式只能有一行代码的情况下才能简化为一行,如果有多行,那么就用代码块包裹;
- 前提是接口为函数式接口;
- 多个参数也可以去掉参数类型要去掉就都去掉,必须加上括号。
例:
love = (int a, int b) -> {
System.out.println("1");
System.out.println("2");
}
//简化为
love = (a,b) -> {
System.out.println("1");
System.out.println("2");
}
四、线程的状态
1、线程状态相关API
- setPriority(int newPriority) 更改线程的优先级
- static void sleep(long millis) 在指定的毫秒内让当前正在执行的线程休眠
- void join() 等待该线程终止
- static void yield() 暂停当前正在执行的线程对象,并执行其他线程
- void interrupt() 中断线程(不建议使用)
- boolean isAlive() 测试线程是否处于活状态
2、线程休眠 sleep
- sleep(时间)指定当前线程阻塞的毫秒数;
- sleep存在异常InterruptedException;
- sleep时间达到后线程进入就绪状态;
- sleep可以模拟网络延时,到计时等;
- 每一个对象都有一个锁,sleep不会释放锁;
常用功能:
模拟网络延时、倒计时
//模拟倒计时
public static void tenDown(int num){
while (true){
System.out.println(num--);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(num < 0){
break;
}
}
}
3、线程礼让 yield
- 礼让线程,让当前正在执行的线程暂停,但不阻塞;
- 将线程从运行状态转为就绪状态;
- 让cpu重新调度,礼让不一定成功,看cpu心情
//测试礼让
public class TestYield implements Runnable{
public static void main(String[] args) {
TestYield testYield = new TestYield();
new Thread(testYield,"a").start();
new Thread(testYield,"b").start();
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始执行...");
//礼让
Thread.yield();
System.out.println(Thread.currentThread().getName() + "结束执行。");
}
}
4、线程合并 join
- Join合并线程,待此线程执行完成之后,再执行其他线程,其他线程阻塞;
- 可以想象成插队
//测试Join方法(可以想象为插队)
public class JoinTest implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("线程vip来了:" + i);
}
}
public static void main(String[] args) throws InterruptedException {
//启动vip线程
//现在和main线程在同一个起跑线,当main线程到100时,礼让线程vip
JoinTest joinTest = new JoinTest();
Thread thread = new Thread(joinTest);
thread.start();
//main线程计数
for (int i = 0; i < 200; i++) {
if(i==100){
thread.join();//插队
}
System.out.println("main线程" + i);
}
}
}
5、线程状态观测 State
- NEW
尚未启动的线程处于此状态。 - RUNNABLE
在Java虚拟机中执行的线程处于此状态。 - BLOCKED
被阻塞的等待监视器锁定的线程处于此状态。 - WAITING
正在等待另一个线程执行特定动作的线程处于此状态。 - TIMED WATING
正在等待另一个线程执行动作达到指定等待时间的线程处于此状态。 - TERMINATED
已退出的线程处于此状态。
5、线程优先级 Priority
-
Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。
-
线程的优先级用数字表示,范围1~10
Thread.MIN_PRIORITY = 1; -
获取/改变优先级
getPriority() / setPriority(int xxx)
优先级低只是意味着获得调度的概率低,并不是优先级低就不会被调用了,这都是看CPU的调度。
5、守护线程 daemon
- 线程分为用户线程和守护线程;
- 虚拟机必须确保用户线程执行完毕(main线程);
- 虚拟机不用等待线程执行完毕;
- 如,后台记录操作日志、监控内存、垃圾回收等待;
五、线程同步
多个线程操作同一个对象
1、线程同步
处理多线程问题时,多个线程访问同一个对象(并发问题),并且某个线程还想修改这个对象。这时候就需要线程同步,线程同步其实就是一种等待机制,多个需要同时访问此对象的进程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程再使用。
例子:
//不安全的买票
public class UnsafeBuyTicket {
public static void main(String[] args) {
BuyTicket buyTicket = new BuyTicket();
Thread th1 = new Thread(buyTicket,"小明");
Thread th2 = new Thread(buyTicket,"小红");
Thread th3 = new Thread(buyTicket,"小毛");
th1.start();
th2.start();
th3.start();
}
}
class BuyTicket implements Runnable{
private int ticketNums = 10;//票
boolean flag = true;//外部停止方式
@Override
public void run() {
//买票
while (flag){
try {
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void buy() throws InterruptedException {
//判断是否有票
if(ticketNums <= 0){
flag = false;
return;
}
//模拟延时
Thread.sleep(1000);
//买票
System.out.println(Thread.currentThread().getName() + "买到了第" + ticketNums-- + "张票");
}
}
分析为什么会出现三个线程买同一张票的问题(重要!)
JMM(Java Memory Model),是一种基于计算机内存模型,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。保证共享内存的原子性、可见性、有序性(这三个也是多线程的三大特性,划重点,面试经常问)。
可见性:
- 多线程操作共享内存时,执行结果能够及时的同步到共享内存,确保其他线程对此结果及时可见。
- 同一进程下的多个线程,内存资源是共享的。主内存的count才是共享资源。程序猿学社、隔壁老王、小张,实际上不是直接对主内存的count进行写入操作。实际上,程序运行过程中,他们每个人,都有各自的工作内存。实际上就是把主内存的count,每个人,都copy一份,对各自的工作内存的变量进行操作。操作完后,再把对应的结果通知到主内存。
说了这么多,我们这时候应该知道之前写的模拟抢票demo为什么会有线程安全问题了把。就是因为各自都操作自己的工作内存,拿到主内存的值就开始操作。假设,这时候count为40,同一时间,来了三个线程,那这三个线程的工作内存拿到的值都是40,这样就会导致,这三个线程,都会抢到39这张票。
2、队列 和 锁
- 同步方法
//安全的买票
public class UnsafeBuyTicket {
public static void main(String[] args) {
BuyTicket buyTicket = new BuyTicket();
Thread th1 = new Thread(buyTicket,"小明");
Thread th2 = new Thread(buyTicket,"小红");
Thread th3 = new Thread(buyTicket,"小毛");
th1.start();
th2.start();
th3.start();
}
}
class BuyTicket implements Runnable{
private int ticketNums = 10;//票
boolean flag = true;//外部停止方式
@Override
public void run() {
//买票
while (flag){
try {
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//synchronized 同步方法,锁的是this
public synchronized void buy() throws InterruptedException {
//判断是否有票
if(ticketNums <= 0){
flag = false;
return;
}
//模拟延时
Thread.sleep(100);
//买票
System.out.println(Thread.currentThread().getName() + "买到了第" + ticketNums-- + "张票");
}
}
- 同步代码块
//线程安全的集合
public class UnsafeList {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
for (int i = 0; i < 10000; i++) {
new Thread(() -> {
//synchronized同步块
synchronized (list) {
list.add(Thread.currentThread().getName());
}
}).start();
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size());
}
}
3、死锁
多个线程各自占有一些共享的资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。某一个同步代码块同时拥有“两个以上对象的锁”,就可能发生“死锁”问题。
4、Lock锁
- 从JDK5开始,Java提供了更强大的线程同步机制——通过显示定义同步锁对象来实现同步。同步锁使用Lock对象充当,
- ReentrantLock类实现了Lock,他拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentanctLock,可以显示加锁、释放锁。
synchronized与Lock的对比
- Lock是显示锁(手动开启和关闭锁,别忘记关闭锁)synchronized是隐式锁,出了作用域自定释放;
- Lock只有代码块锁,synchronized有代码块锁和方法锁;
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
- 优先使用顺序:
Lock > 同步代码块(已经进入了方法体,分配了相应资源)> 同步方法(在方法体之外)
六、线程协作
1、线程通信相关API
- wait() 表示线程一直等待,直到其他线程通知,与sleep不同,会释放锁;
- notify() 唤醒一个处于等待状态的线程;
- notifyAll() 唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先调度;
//1、测试生产者和消费者模型 --> 利用缓冲区解决:管程法
//生产者,消费者,产品,缓冲区
public class TestPC {
public static void main(String[] args) {
Container container = new Container();
Productor productor = new Productor(container);
Consumer consumer = new Consumer(container);
new Thread(productor).start();
new Thread(consumer).start();
}
}
//生产者
class Productor implements Runnable{
Container container = new Container();
public Productor(Container container) {
this.container = container;
}
//生产
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("生产了" + i + "只鸡");
container.push(new Chiecken(i));
}
}
}
//消费者
class Consumer implements Runnable{
Container container = new Container();
public Consumer(Container container) {
this.container = container;
}
//消费
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("消费了-->" + container.pop().id + "只鸡");
}
}
}
//产品
class Chiecken{
int id;
public Chiecken(int id) {
this.id = id;
}
}
//缓冲区
class Container{
//需要一个容器存放产品
Chiecken[] chieckens = new Chiecken[10];
//容器计数器
int count = 0;
//生产者放入产品
public synchronized void push(Chiecken chiecken){
//如果容器满了,就需要等待消费者消费
if(count == chieckens.length){
//通知消费者消费,生产等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果没满,我们就需要丢入产品
chieckens[count] = chiecken;
count ++;
//可以通知消费者消费了
this.notifyAll();
}
//消费者消费产品
public synchronized Chiecken pop(){
if(count == 0){
//等待生产者生产,消费者等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果可以消费
count --;
Chiecken chiecken = chieckens[count];
//吃完了,等待消费者生产
this.notifyAll();
return chiecken;
}
}
//2、解决生产者和消费者问题 信号灯法
public class TestPC2 {
public static void main(String[] args) {
Show show = new Show();
Player player = new Player(show);
Watcher watcher = new Watcher(show);
new Thread(player).start();
new Thread(watcher).start();
}
}
//演员
class Player implements Runnable{
Show show;
public Player(Show show){
this.show = show;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i%2 == 0){
this.show.play("钢铁侠");
}else {
this.show.play("绿巨人");
}
}
}
}
//观众
class Watcher implements Runnable{
Show show;
public Watcher(Show show) {
this.show = show;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
this.show.watch();
}
}
}
//产品————>表演
class Show {
//演员表演,观众等待 T
//观众观看,演员等待 F
boolean flag = true;
String voice;
//表演
public synchronized void play(String voice) {
if(!flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("表演了" + voice);
this.voice = voice;
//通知观看
this.notifyAll();
flag = !flag;
}
//观看
public synchronized void watch(){
if(flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("观看了" + voice);
//通知表演
this.notifyAll();
flag = !flag;
}
}
2、线程池
背景:经常创建和销毁、使用大量特别大的资源,比如并发情况下的线程,对性能影响很大,使用线程池便于线程管理。
- ExecutorService
- Executors
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
{
}