Java基础-多线程
一、概述
**进程 :**是一个正在执行中的程序,每一个进程执行都有一个执行顺序,该顺序是一个执行路径,或者叫一个控制单元;
**线程:**就是进程中的一个独立控制单元,线程在控制着进程的执行。一个进程中至少有一个进程。
我们在计算机中经常要并发的执行很多事情,比如说边打游戏边听歌这种,但是如果按之前我们学习的方法来进行实现的话,我们是只能打完游戏再听歌,一件事一件事的去做,所以说我们在编写程序的时候需要使用多线程来进行编写。
二、线程创建
线程的创建主要由三种方法、继承Thread类、实现Runnable接口以及实现Callable接口
下面我们来依次看看这三种方式
2.1 继承Thread类
具体步骤主要由三步
- 继承Thread类
- 重写其中的run方法
- 通过调用线程的start方法进行调用
具体例子如下
public class ThreadTest1 extends Thread{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("zjj帅的"+i);
}
}
public static void main(String[] args) {
ThreadTest1 threadTest1 = new ThreadTest1();
threadTest1.start();
for (int i = 0; i < 200; i++) {
System.out.println("zjj丑的"+i);
}
}
}
最后可以发现二者是并发执行。
2.2 实现runnable接口
具体步骤如下
- 实现Runnable接口,写run方法
- 将自己写好的Runnable传入新Thread的构造器
- 调用start方法
改写一下上面的例子
public class ThreadTest1 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("zjj帅的"+i);
}
}
public static void main(String[] args) {
ThreadTest1 threadTest1 = new ThreadTest1();
new Thread(threadTest1).start();//传入新的Thread的构造中,这里运用了一个代理模式的设计思想
for (int i = 0; i < 200; i++) {
System.out.println("zjj丑的"+i);
}
}
}
执行结果与之前相同
这里我们推荐使用继承runnable接口的方式来创建进程
!(java单继承机制,你要继承thread类之后就不好继承别的嘞)
写一个龟兔赛跑的例子!
package ThreadJ;
public class Race implements Runnable {
boolean flag = false;
int steps;//跑了多少步
String winner;
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
steps = i;
flag = IsGameOver(steps);//判断是否有人已经完成比赛了
if(!flag) {
System.out.println(Thread.currentThread().getName() + "跑了" + i + "步");
}else{
System.out.println("胜利者是"+winner);
break;
}
}
}
private boolean IsGameOver(int stepss) {
if(flag){
return true;
}
if(stepss>=100){
winner=Thread.currentThread().getName();
return true;
}
return false;
}
public static void main(String[] args) {
Race race = new Race();
new Thread(race,"兔子").start();
new Thread(race,"乌龟").start();
}
}
2.2 实现callable接口(了解)
具体步骤:
例子:
package ThreadJ;
import java.util.concurrent.*;
public class ThreadTest2 implements Callable<Boolean> {
@Override
public Boolean call() throws Exception {
for (int i = 0; i < 20; i++) {
System.out.println("zjj帅的"+i);
}
return true;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
ThreadTest2 tt1 = new ThreadTest2();
ThreadTest2 tt2 = new ThreadTest2();
//创建执行服务
ExecutorService es = Executors.newFixedThreadPool(2);
//提交服务
Future<Boolean> submit1 = es.submit(tt1);
Future<Boolean> submit2 = es.submit(tt2);
//获取结果
System.out.println(submit1.get());
System.out.println(submit2.get());
//关闭
es.shutdownNow();
}
}
最后执行的结果为:
三、线程的操作
线程主要有新建状态、就绪状态、运行状态、阻塞状态以及死亡状态。
下面我们要把与这五个状态相互转换时有关的操作进行详解
3.1 线程的停止
在这里我们不建议使用Thread类中存在的一些让线程停止的方法,比如说自由的stop和destroy方法
我们建议编写一个外部类控制线程是否停止。
public class ThreadStopTest implements Runnable{
boolean flag = true;
@Override
public void run() {
while (flag) {
System.out.println("线程正在运行!");
}
}
public void stop()
{
this.flag = false;
}
public static void main(String[] args) {
ThreadStopTest threadTest1 = new ThreadStopTest();
new Thread(threadTest1).start();
for (int i = 0; i < 200; i++) {
System.out.println("zjj帅的"+i);
if(i==188){//当i = 188时让threadTest1停止
threadTest1.stop();
}
}
}
}
我们可以发现,在执行第188次循环时,创建的进程停止了!
3.2 线程的休眠
线程的休眠非常好理解,就是线程在执行的过程中“停一小会儿”,他主要使用Thread类中的Sleep方法,以下是一个按秒输出当前系统时间的例子。
public class ThreadSleepTest {
public static void main(String[] args) throws InterruptedException {
while (true) {
nowTime();
}
}
public static void nowTime() throws InterruptedException {
Date nowtime = new Date();
System.out.println(new SimpleDateFormat("HH:mm:ss").format(nowtime));
Thread.sleep(1000);
}
}
输出结果就是他!
3.3 线程的让步
线程的让步是指当前在cpu中的线程在执行的过程中退出cpu,然后各个线程再重新竞争cpu使用的操作,具体在代码中使用yield来执行
下面我们来看一个例子
package ThreadJ;
public class ThreadYieldTest {
public static void main(String[] args) {
myYield y1 = new myYield();
myYield y2 = new myYield();
new Thread(y1,"zjj").start();
new Thread(y2,"dirtyLily").start();
}
static class myYield implements Runnable{//用了内部类
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"正在运行哦!");
Thread.yield();
System.out.println(Thread.currentThread().getName()+"停止运行哦!");
}
}
}
最后输出结果是:
3.4 线程强制执行
线程强制执行就是在其他线程正在执行的时候,其他进程强制插队执行的过程,在代码中常用join来实现线程的强制执行,事例如下。
package ThreadJ;
public class ThreadJoinTest implements Runnable{
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println("vip线程来啦!"+i);
}
}
public static void main(String[] args) throws InterruptedException {
ThreadJoinTest th = new ThreadJoinTest();
Thread thread = new Thread(th);
thread.start();
for (int i = 0; i < 1000; i++) {
if(i==200){//在I等于200时让thread强制插队
thread.join();
}
System.out.println("主进程在此"+i);
}
}
}
最后输出结果正确!
3.5 查看线程状态
线程在我们的java中主要由以下五个状态标志
-
NEW
至今尚未启动的线程处于这种状态。 -
RUNNABLE
正在 Java 虚拟机中执行的线程处于这种状态。 -
BLOCKED
受阻塞并等待某个监视器锁的线程处于这种状态。 -
WAITING
无限期地等待另一个线程来执行某一特定操作的线程处于这种状态。 -
TIMED_WAITING
等待另一个线程来执行取决于指定等待时间的操作的线程处于这种状态。 -
TERMINATED
已退出的线程处于这种状态。
在代码中我们可以使用Thread.getState()观测当前线程状态。
下面是一个观测线程状态的小例子!
package ThreadJ;
public class ThreadState{
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(
()->{
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
);
System.out.println(thread.getState());//new
thread.start();
System.out.println(thread.getState());//runnable
while (thread.getState()!= Thread.State.TERMINATED){
Thread.sleep(100);
System.out.println(thread.getState());//TIMED_WAITING(start方法中让其睡了五秒)
}
}
}
3.6 线程的优先级
java中提供一个线程调度器来监测所有进入就绪状态的线程,其将按照线程优先级决定应该调度哪个线程来执行。
其优先级用数字表示
我们可以使用thread.setPriority()来改变线程优先级
注意:优先级低只是意味着获得调度的概率低,不代表他就不会被调用了。。。
3.7 守护线程
java中主要由两种类别的线程:用户线程以及守护线程
我们平常编写的线程默认都是用户线程,比如说我们的main线程,这些是虚拟机必须执行的,只有在他们执行完毕的时候虚拟机才能退出。
守护线程与普通线程的唯一区别是:当JVM中所有的线程都是守护线程的时候,JVM就可以退出了;如果还有一个或以上的非守护线程则不会退出。(以上是针对正常退出,调用System.exit则必定会退出)
通过setDeamon(true)可以将一个线程设置为守护线程。
所以setDeamon(true)的唯一意义就是告诉JVM不需要等待它退出,让JVM喜欢什么退出就退出吧,不用管它。
四、线程同步
在我们使用许多线程操作同一个对象时就需要考虑其相关的同步性问题。
比如说你和你家里人同时去银行取钱,一个用手机银行取,一个用柜台取,你们俩其中一个人在取的时候如果另一个人也取,你们两个加起来取得钱如果超过了你银行卡余额的话,银行卡余额就是负的了,这显然是不可能的。
在线程中也是一样,当一个线程操作资源的时候会交给其一把锁,把这个资源锁住使其他资源无法访问,以达到线程同步的目的,在使用后释放相关锁。不过这种机制也会产生一些问题
- 一个线程持有锁的话其他线程需要挂起等待(影响性能)
- 容易发生优先级倒置(低优先级线程半天不释放锁)
4.1 synchronized 关键字
synchronized的主要用法有两种:同步方法和同步块。
synchronized方法对控制对象的访问,每个对象都会有一把锁,每个synchronized方法都必须获取到相关锁才可以执行。
优点:简单的实现线程同步
缺陷:大大影响效率
例子:
package ThreadJ;
public class ThreadsynTickets {
public static void main(String[] args) {
TicketsStation mm = new TicketsStation();
Thread t1 = new Thread(mm, "xw");
Thread t2 = new Thread(mm, "xl");
Thread t3 = new Thread(mm, "xs");
t1.start();
t2.start();
t3.start();
}
}
class TicketsStation implements Runnable{
private int tickets_num = 10;
boolean flag = true;
@Override
public void run() {
while(flag){
try {
buy();
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private synchronized void buy() throws InterruptedException {
if(tickets_num<=0){
flag = false;
return;
}
System.out.println(Thread.currentThread().getName()+"买了第"+tickets_num--+"张票");
}
}
这里是一个车站买票的例子,三个进程同时在一个车站买票,如果线程不同步的话则会出现以下情况。
我们发现第七张票被两个人同时抢了,这显然是我们不希望看到的情况。
这时我们在buy方法前加上synchronized关键字让其实现线程同步看看
非常有序,其实现了线程同步。
同理,同步块的实现方法是将需要进行线程同步的语句使用
synchronized(obj){
语句;
}
格式进行编写,其中的obj填入系统需要进行增删改查的变量即可。
4.2 死锁
死锁 : 当线程A持有独占锁a,并尝试去获取独占锁b的同时,线程B持有独占锁b,并尝试获取独占锁a的情况下,就会发生AB两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。
(常考点)
造成死锁的四个条件:
- 互斥条件:一个资源每次只能被一个线程使用。
- 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺。
- 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
4.3 lock锁
除了synchronized锁我们也可以使用lock锁来对相关代码块进行加锁,下面我们改写下之前的那个买票的例子。
package ThreadJ;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadsynTickets {
public static void main(String[] args) {
TicketsStation mm = new TicketsStation();
Thread t1 = new Thread(mm, "xw");
Thread t2 = new Thread(mm, "xl");
Thread t3 = new Thread(mm, "xs");
t1.start();
t2.start();
t3.start();
}
}
class TicketsStation implements Runnable{
private int tickets_num = 10;
boolean flag = true;
ReentrantLock reentrantLock = new ReentrantLock();//这里我们使用一个ReentrantLock
@Override
public void run() {
while(flag){
try {
reentrantLock.lock();//加锁
buy();
Thread.sleep(1000);
reentrantLock.unlock();//解锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void buy() throws InterruptedException {
if(tickets_num<=0){
flag = false;
return;
}
System.out.println(Thread.currentThread().getName()+"买了第"+tickets_num--+"张票");
}
}
五、线程通信
线程之间通信常用的是wait和notify方法。
主要有以下方法:
下面我们用最经典的生产者消费者问题来试一试
package ThreadJ;
//生产者、消费者
public class ThreadPC {
public static void main(String[] args) {
synArea synArea = new synArea();
producer producer = new producer(synArea);
consumer consumer = new consumer(synArea);
producer.start();
consumer.start();
}
}
class producer extends Thread{
synArea s;
public producer(synArea s){
this.s = s;
}
@Override
public void run() {
//生产
for (int i = 0; i < 100; i++) {
try {
s.push(new chicken(i+1));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("生产者生产了第"+i+"只鸡");
}
}
}
class consumer extends Thread{
synArea s;
public consumer(synArea s){
this.s = s;
}
@Override
//消费
public void run() {
for (int i = 0; i < 100; i++) {
try {
chicken pop = s.pop();
System.out.println("消费者消费了第"+pop.num+"只鸡");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class chicken{
public int num;
public chicken(int num) {
this.num = num;
}
}
class synArea{
chicken[] chick = new chicken[10];
int count = 0;
//生产者生产
public synchronized void push(chicken chicken) throws InterruptedException {
//缓冲区满,等待
if(count==chick.length) {
this.wait();
}
chick[count] = chicken;
count++;
this.notifyAll();
}
public synchronized chicken pop() throws InterruptedException {
//缓冲区空,等待。
if(count==0) {
this.wait();
}
count--;
chicken chi = chick[count];
this.notifyAll();
return chi;
}
}
当然还有一种方法是用信号量进行控制的,这里就不多赘述了,大家有兴趣可以去看看
六、线程池入门
为什么要用线程池?
- 降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
- 提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行
- 方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造 成cpu过度切换(cpu切换线程是有时间成本的(需要保持当前执行线程的现场,并恢复要执行线程的现场))。
- 提供更强大的功能,延时定时线程池。
基本参数:
1、corePoolSize(线程池基本大小):当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize时,(除了利用提交新任务来创建和启动线程(按需构造),也可以通过 prestartCoreThread() 或 prestartAllCoreThreads() 方法来提前启动线程池中的基本线程。)
2、maximumPoolSize(线程池最大大小):线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。另外,对于无界队列,可忽略该参数。
3、keepAliveTime(线程存活保持时间)当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数。
4、workQueue(任务队列):用于传输和保存等待执行任务的阻塞队列。
5、threadFactory(线程工厂):用于创建新线程。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)。
下面我们通过一个简单例子看看线程池的操作吧…
package ThreadJ;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolTest {
public static void main(String[] args) {
//创建一个大小为10的线程池
ExecutorService es = Executors.newFixedThreadPool(10);
//执行一手
es.execute(new MyThread());
es.execute(new MyThread());
es.execute(new MyThread());
//关闭
es.shutdown();
}
}
class MyThread implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
成功输出!
完结撒花!!