Java学习——多线程
一 一些概念
进程:一个内存中运行的应用程序,每个进程都有一个独立的内存空间。
线程:进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行。一个进程最少有一个线程。(线程实际上是在进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分成若干个线程。Eg:音乐播放器有播放歌曲、显示歌词、显示界面。)
同步:排队执行 , 效率低但是安全。
异步:同时执行 , 效率高但是数据不安全。
并发:指两个或多个事件在同一个时间段内发生。
并行:指两个或多个事件在同一时刻发生(同时发生)。
二 继承Thread
使用流程:
1.自定义一个类,继承Thread类,重写run方法。
2.main方法中,创建新编写的类的对象,调用start方法执行线程。
public class Deno {
public static void main(String[] args) {
MyThread m = new MyThread();
m.start();
}
}
public class MyThread extends Thread {
// run方法就是线程要执行的任务方法
public void run() {
// 这里的代码就是一条新的执行路径
// 这个执行路径的触发方式不是调用run方法,而是通过thread对象的start方法来启动任务
}
}
public class Deno {
public static void main(String[] args) {
MyThread m = new MyThread();
m.start();
for (int i = 0; i < 10; i++) {
System.out.println("汗滴禾下土"+i);
}
}
}
public class MyThread extends Thread {
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("锄禾日当午"+i);
}
}
}
第一次运行结果:汗滴禾下土0 锄禾日当午0 汗滴禾下土1 锄禾日当午1 ··· 汗滴禾下土6 锄禾日当午6 锄禾日当午7 汗滴禾下土7 锄禾日当午8 汗滴禾下土8 锄禾日当午9 汗滴禾下土9
第二次运行结果:汗滴禾下土0 锄禾日当午0 汗滴禾下土1 锄禾日当午1 ··· 汗滴禾下土3 锄禾日当午3 锄禾日当午4 ··· 锄禾日当午9 汗滴禾下土4 ··· 汗滴禾下土9
原因:两个线程谁快谁慢不确定,所以每次允许结果都会不同。
三 实现Runnable
public class Deno {
public static void main(String[] args) {
// 1 创建一个任务对象
MyRunnable r = nwe MyRunnable();
// 2 创建一个线程,并为其分配一个任务
Thread t = new Thread(r);
// 3 执行这个线程
t.start();
}
}
public class MyRunnable implements Runnable{
public void run() { // 线程的任务 }
}
public class Deno {
public static void main(String[] args) {
// 1 创建一个任务对象
MyRunnable r = nwe MyRunnable();
// 2 创建一个线程,并为其分配一个任务
Thread t = new Thread(r);
// 3 执行这个线程
t.start();
for (int i = 0; i < 10; i++) {
System.out.println("汗滴禾下土"+i);
}
}
}
public class MyRunnable implements Runnable{
@Override
public void run() {
// 线程的任务
for (int i = 0; i < 10; i++) {
System.out.println("锄禾日当午"+i);
}
}
}
实现Runnable 比 继承 Thread 有以下优势:
1,通过创建任务然后给线程分配的方式来实现多线程,更适合多个线程同时执行相同任务的情况;
2,可以避免单继承带来的局限性;
3,任务与线程本身是分离的,提高了程序的健壮性;
4,后续学习的线程池技术接受Runnable类型的任务,不接收Thread类型的线程。
四 Thread类
1 设置与获取线程名称
String getName(); // 返回此线程的名称。
public class Demo3 {
public static void main(String[] args) {
// 如何获取线程的名称
System.out.println(
Thread.currentThread().getName());
// 不设置的有默认的名字
new Thread(new MyRunnable()).start();
new Thread(new MyRunnable()).start();
new Thread(new MyRunnable()).start();
// 设置线程名称的方式1
Thread t = new Thread(new MyRunnable());
t.setName("wwww");
t.start();
// 设置线程名称的方式2
new Thread(new MyRunnable(),"锄禾日当午").start();
}
static class MyRunnable implements Runnable{
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
}
输出结果(每次执行顺序会不同):
main Thread-0 锄禾日当午 wwww Thread-2 Thread-1
2 线程休眠sleep
static void sleep(long millis):导致当前正在执行的线程休眠(暂时停止执行)指定的毫秒数,具体取决于系统计时器和调度程序的精度和准确性。
Thread.sleep(1000); //1000毫秒
public class Demo4 {
public static void main(String[] args) throws InterruptedException {
//线程的休眠
for (int i = 0; i < 10; i++) {
System.out.println(i);
Thread.sleep(1000); //1000毫秒
}
}
}
输出结果:每隔1s输出0、1、2、3···9
线程阻塞:所有消耗时间的操作。不止指线程休眠。如文件读取、接受用户输入。
3 线程中断
void interrupt(); // 中断此线程。
public class Demo5 {
public static void main(String[] args) {
// 一个线程是一个独立的执行路径,它是否结束应该由其自身决定
Thread t1 = new Thread(new MyRunnable());
t1.start();
for (int i = 0; i < 5; i++) {
Sout(Thread.currentThread().getName()+":"+i); // main+i
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 给线程t1添加中断标记
t1.interrupt();
}
static class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
Sout(Thread.currentThread().getName()+":"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) { //线程中断异常
//e.printStackTrace();
System.out.println("发现了中断标记,线程自杀");
return;
}
}
}
}
}
输出结果:Thread-0:0 main:0 Thread-0:1 main:1 Thread-0:2 main:2 Thread-0:3 main:3
Thread-0:4 main:4 Thread-0:5 发现了中断标记,线程自杀
无return的输出结果:main:0 Thread-0:0 main:1 Thread-0:1 main:2 Thread-0:2
main:3 Thread-0:3 main:4 Thread-0:4 Thread-0:5 发现了中断标记,线程自杀
Thread-0:6 Thread-0:7 Thread-0:8 Thread-0:9
五 线程不安全
1,现象与原因
public class Demo {
public static void main(String[] args) {
Runnable run = new Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable{
//总票数
private int count = 5;
@Override
public void run() {
while (count > 0){
// 卖票
System.out.print("正在准备卖票 ");
// 加入休眠sleep
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count --;
System.out.println("卖票结束,余票:"+count);
}
}
}
}
输出结果:
正在准备卖票 正在准备卖票 正在准备卖票 卖票结束,余票:4
正在准备卖票 卖票结束,余票:3
正在准备卖票 卖票结束,余票:2
正在准备卖票 卖票结束,余票:1
正在准备卖票 卖票结束,余票:-1
卖票结束,余票:0
卖票结束,余票:-2
出现问题:余票负数;(count更大或者多次运行会出现)多个同一“余票i”。
原因:线程A、B、C抢票。仅剩1张票时,A在休眠时,B、C因为count>1再次进入run()程序。A休眠后抢到最后一张票,B、C要走完全部程序。
2,解决方案
同步代码块和同步方法都属于隐式锁。
(1)同步代码块
格式:synchronized(锁对象){ }
多个线程观察Object o是否已经打上了锁的标记。打了,表示有线程正在执行,其他线程要等待。一旦这一程序结束,标记解开,所有线程开始抢,打标记。
class MyRunnable implements Runnable {
//锁对象,多个线程执行此任务时,成员属性为同一个
private Object o = new Object();
@Override
public void run() {
//同步代码快
synchronized (o) {
if () {}
}
}
}
public class Demo8 {
public static void main(String[] args) {
Object o = new Object();
Runnable run = new Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable{
private int count = 10;
private Object o = new Object(); // o放这,是同一把锁
@Override
public void run() {
// o放这,不是同一把锁,锁不住,还是会出现负票的结果
// Object o = new Object();
while (true) {
// synchronized(锁对象){ }
synchronized (o) {
if (count > 0) {
System.out.print("正在准备卖票 ");
// 加入休眠sleep
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count --;
// 加上是哪个线程抢到的票
Sout(Thread.currentThread().getName()+"卖票结束,余票"+count);
} else {
break;
}
}
}
}
}
}
运行结果:
正在准备卖票 Thread-0卖票结束,余票:9
正在准备卖票 Thread-0卖票结束,余票:8
正在准备卖票 Thread-1卖票结束,余票:7
正在准备卖票 Thread-2卖票结束,余票:6
正在准备卖票 Thread-1卖票结束,余票:5
正在准备卖票 Thread-1卖票结束,余票:4
正在准备卖票 Thread-0卖票结束,余票:3
正在准备卖票 Thread-0卖票结束,余票:2
正在准备卖票 Thread-0卖票结束,余票:1
正在准备卖票 Thread-1卖票结束,余票:0
注:每次运许结果中,Thread-i中i不同,一直为0也可能(多线程)。下同。
(2)同步方法
线程同步:synchronized
class MyThread extends Thread {
public synchronized void xxx() {}
//此时锁对象为this
public synchronized static void xxx() {}
//此时锁对象为MyThread.class
}
public class Demo9 {
public static void main(String[] args) {
Object o = new Object();
Runnable run = new Ticket(); // 同一把锁
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable{
private int count = 10;
@Override
public void run() {
while (true) {
boolean flag = sale();
if(!flag){
break;
}
}
}
// 加synchronized
public synchronized boolean sale(){
if (count > 0) {
System.out.print("正在准备卖票 ");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
Sout(Thread.currentThread().getName()+"卖票结束,余票:"+count);
return true;
}
return false; // 票卖完返回false,run()不再执行
}
}
}
输出结果:
正在准备卖票 Thread-0卖票结束,余票:9
正在准备卖票 Thread-0卖票结束,余票:8
正在准备卖票 Thread-0卖票结束,余票:7
正在准备卖票 Thread-2卖票结束,余票:6
正在准备卖票 Thread-2卖票结束,余票:5
正在准备卖票 Thread-2卖票结束,余票:4
正在准备卖票 Thread-2卖票结束,余票:3
正在准备卖票 Thread-2卖票结束,余票:2
正在准备卖票 Thread-1卖票结束,余票:1
正在准备卖票 Thread-1卖票结束,余票:0
(3)显式锁
线程同步:显式锁 Lock 子类 ReentrantLock
class MyRunnable implements Runnable {
private Lock l = new ReentrantLock();
//默认false,表示非公平锁,传入true则代表公平锁,先到先得,可防止回首掏
@Override
public void run() {
l.lock(); //启动锁
if () {}
l.unlock(); //关闭锁
}
}
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Demo10 {
public static void main(String[] args) {
Object o = new Object();
Runnable run = new Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable{
private int count = 10;
// 参数为true表示公平锁 默认是false,不是公平锁
private Lock l = new ReentrantLock(fair:true);
@Override
public void run() {
while (true) {
l.lock(); // 锁住
if (count > 0) {
System.out.print("正在准备卖票 ");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
Sout(Thread.currentThread().getName()+"卖票结束,余票:"+count);
}else {
break;
}
l.unlock(); // 锁放开
}
}
}
}
显式锁与隐式锁的区别:https://www.cnblogs.com/kaigejava/p/12710602.html
非公平锁:大家一起抢。上面介绍的3个锁都是非公平锁。
公平锁:排队,哪个线程先来就先执行。在Lock的基础上加上判定条件实现。
六 线程死锁
生活例:A进a房,B进b房。A(想进b)等B出来,B(想进a)等A出来。僵住。
七 多线程通信
如:A线程下载音乐,B线程播放音乐。A线程下载完成后通知B线程播放音乐。
void wait();
//导致当前线程等待它被唤醒,通常是通知或中断。
void wait(long timeoutMillis);
//导致当前线程等待它被唤醒,通常是通知或中断 ,或者直到经过一定量的实时。
void wait(long timeoutMillis, int nanos);
//导致当前线程等待它被唤醒,通常是通知或中断 ,或者直到经过一定量的实时。
notify();
//通常用于唤醒wait()状态的线程。
八 线程的六种状态
- new 尚未启动的状态
- Runnable 在java虚拟机中执行的线程处于此状态
- Blocked 排队等待执行的状态
- Waiting 等待被唤醒的状态
- Timed Waiting 等待被唤醒/等待若干时间的状态
- Terminated 退出的状态
九 带返回值的线程Callable
Java的第三种线程。用得少,仅了解。
Thread和Runnable线程是和主线程并发运行。Callable既可以和主线程并发运行,也可以允许主线程等Callable执行完毕,拿到Callable的返回值再运行。Callalble接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
1,接口定义
//Callable接口
public interface Callable<V> {
V call() throws Exception;
}
//Runnable接口
public interface Runnable {
public abstract void run();
}
2,使用方法
1. 编写类实现Callable接口 , 实现call方法
class XXX implements Callable<T> {
@Override
public <T> call() throws Exception {
return T;
}
}
2. 创建FutureTask对象, 并传入第一步编写的Callable类对象
FutureTask<Integer> future = new FutureTask<>(callable);
3. 通过Thread,启动线程
new Thread(future).start();
4. 获取线程返回的结果 FutureTask.get()
V get() :如果需要等待计算完成,然后检索其结果。
V get(long timeout, TimeUnit unit) :
如果需要,最多等待计算完成的给定时间,然后检索其结果(如果可用)。
3,案例
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Demo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<Integer> c = new MyCallable();
FutureTask<Integer> task = new FutureTask<>(c);
new Thread(task).start();
Integer j = task.get();
System.out.println("返回值为:"+j);
for (int i = 0 ; i < 10 ; i++){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print(i);
}
}
static class MyCallable implements Callable<Integer>{
public Integer call() throws Exception{
for (int i = 0 ; i < 10 ; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print(i+" ");
}
return 100;
}
}
}
输出结果:
0 1 2 3 4 5 6 7 8 9 返回值为:100
0123456789
4,对比Runnable
相同点:都是接口、都可以编写多线程程序、都采用Thread.start()启动线程
不同点:Runnable没有返回值;Callable可以返回执行结果;Callable接口的call()允许抛出异常;Runnable的run()不能抛出
十 线程池
如果并发的线程数量很多,并且每个线程都执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建和销毁线程需要时间。
线程池就是一个容纳多个线程的容器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量的时间和资源。
线程池的好处:降低资源消耗、提高响应速度、提高线程的可管理性。
Java中的四种线程池:缓存线程池、定长线程池、单线程线程池、周期性任务定长线程池。
1,缓存线程池
无限制长度。
任务加入后的执行流程:
1判断线程池是否存在空闲线程 2存在则使用 3不存在则创建线程并使用
ExecutorService service = Executors.newCachedThreadPool();
service.execute(new Runnable(){});
2,定长线程池
长度是指定的线程池。
加入任务后的执行流程:
1 判断线程池是否存在空闲线程
2 存在则使用
3 不存在空闲线程 且线程池未满的情况下 则创建线程 并放入线程池中 然后使用
4 不存在空闲线程 且线程池已满的情况下 则等待线程池的空闲线程
// 传入数字代表有几个线程
ExecutorService service = Executors.newFixedThreadPool(2);
service.execute(new Runnable(){});
3,固定线程池
执行流程:
1 判断线程池的那个线程是否空闲
2 空闲则使用
3 不空闲则等待它空闲后再使用
ExecutorService service = Executors.newSingleThreadExecutor();
service.execute(new Runnable(){});
4,周期定长线程池
执行流程:
1 判断线程池是否存在空闲线程
2 存在则使用
3 不存在空闲线程 且线程池未满的情况下 则创建线程 并放入线程池中 然后使用
4 不存在空闲线程 且线程池已满的情况下 则等待线程池的空闲线程
周期性任务执行时: 定时执行;当某个任务触发时 自动执行某任务
ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
service.schedule();
// 或
service.scheduleAtFixedRate();
public class Demo16 {
public static void main(String[] args) {
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
// 定时执行一次
// 参数:定时执行的任务、时长数字、数字时长的时间单位(Timeunit的常量指定)
scheduledExecutorService.schedule(new Runnable() {
public void run() {Sout(Thread.currentThread().getName()+"锄");}
},delay:5, TimeUnit.SECONDS); //5秒钟后执行
/*
周期性执行任务
参数1:任务
参数2:延迟时长数字(第一次在执行上面时间以后)
参数3:周期时长数字(每隔多久执行一次)
参数4:时长数字的单位
*/
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
public void run() {Sout(Thread.currentThread().getName()+"锄");}
},initialDelay:5,period:1,TimeUnit.SECONDS);
}
}