1、多线程简介
1.1、进程
进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位。
进程的特征:
1)独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间 。 在没有经过进程本身允许的情况下, 一个用户进程不可以直接访问其他进程的地址空间 。
2)动态性:
3)并发性:
1.2、线程
1.2.1、多线程和普通方法
1.2.2、线程与进程
线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程。
(1)一个进程可以包含若干个线程;
(2)线程就是独立的执行路径;
(3)在程序运行时,即使没有自己创建线程、后台也会有多个线程,如主线程,gc线程;
(4)同一个进程的线程之间会进行内存资源的交互,但不同进程之间独享各自分配的内存资源;
(5)main()也是线程,称之为主线程,为系统的入口,用于执行整个程序。
2、线程创建的方法
线程的创建方法有三种分别是继承Thread类、实现Runnable接口以及实现Callable接口,但是第三种再生产开发中几乎见不到,所以主要描述前两种线程创建方法。
image.png
2.1、继承Thread类创建线程
public class TestThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("我在学习线程"+i);
}
}
public static void main(String[] args) {
TestThread testThread = new TestThread();
testThread.start();
for (int i = 0; i < 20; i++) {
System.out.println("我在看代码"+i);
}
}
}
类继承Thread,重写run(),编写线程执行体;在main方法中使用start方法启动线程。
小案例:使用三个线程同时下载网上图片:
public class TestThread2 extends Thread {
private String url;
private String name;
public TestThread2(String url, String name){
this.url = url;
this.name = name;
}
@Override
public void run() {
WebDownloader webDownloader = new WebDownloader();
webDownloader.downloader(url,name);
}
public static void main(String[] args) {
TestThread2 t1 = new TestThread2("https://imagepphcloud.thepaper.cn/pph/image/126/215/304.jpg","1.jpg");
TestThread2 t2 = new TestThread2("https://imagepphcloud.thepaper.cn/pph/image/126/215/307.jpg","2.jpg");
TestThread2 t3 = new TestThread2("https://imagepphcloud.thepaper.cn/pph/image/126/215/307.jpg","3.jpg");
t1.start();
t2.start();
t3.start();
}
}
class WebDownloader{
public void downloader(String url, String name){
try {
FileUtils.copyURLToFile(new URL(url),new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("IO异常,Downloader方法出现问题");
}
}
}
2.2、实现Runnable接口创建线程
public class TestThread3 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("Runnable线程");
}
}
public static void main(String[] args) {
TestThread3 testThread3 = new TestThread3();
new Thread(testThread3).start();
for (int i = 0; i < 100; i++) {
System.out.println("主线程....");
}
}
}
3、必要知识点
3.1、静态代理
静态代理:使用一个代理对象来实现真实对象的方法,同时在不修改目标对象的前提下扩展目标对象的功能。
使用一个简单的例子来理解静态代理:假如对象You想要结婚,但是You只能做结婚的动作,那些结婚时候的其他动作,比如办婚礼,装扮现场气氛组You不能做也不想做,就可以找一个婚庆公司来代理这件事。
package ThreadDemo;
/**
* @author : HaiLiang Huang
* @date : 2021年4月16日14:51:52
*/
public class StaticProxy {
public static void main(String[] args) {
You you = new You();
MarryCompany marryCompany = new MarryCompany(you);
marryCompany.marry();
}
}
interface Marry{
void marry();
}
class You implements Marry{
@Override
public void marry() {
System.out.println("秦老师要结婚了,超开心");
}
}
class MarryCompany implements Marry{
private You target;
public MarryCompany(You target){
this.target = target;
}
@Override
public void marry() {
before();
target.marry();
after();
}
private void after() {
System.out.println("完事收钱");
}
private void before() {
System.out.println("婚期公司布置婚礼现场");
}
}
3.2、Lambda表达式:
Lambda表达式形成的步骤(以电影为例子):
1、创建出函数式编程:
interface Move {
void move();
}
2、可以采用类来实现这个接口完成接口动作:
class Move2 implements Move {
@Override
public void move() {
System.out.println("我在看电影1");
}
}
3、可以使用静态内部类来完成接口动作:
public class LambdaTest {
static class Move3 implements Move {
@Override
public void move() {
System.out.println("我在看电影2");
}
}
4、局部内部类完成接口动作:
public static void main(String[] args) {
Move move2 = new Move2();
move2.move();
move2 = new Move3();
move2.move();
class Move4 implements Move {
@Override
public void move() {
System.out.println("我在看电影2");
}
}
Move move4 = new Move4();
move4.move();
5、匿名内部类完场接口方法:
Move move = new Move() {
@Override
public void move() {
System.out.println("我在看电影3");
}
};
move.move();
6、Lambda表达式:
Move move1 = () -> {
System.out.println("我在看电影3");
};
move1.move();
总结:
上述完整代码:
/**
* @author : HaiLiang Huang
* @date : 2021年4月16日15:13:58
*/
public class LambdaTest {
static class Move3 implements Move {
@Override
public void move() {
System.out.println("我在看电影2");
}
}
public static void main(String[] args) {
Move move2 = new Move2();
move2.move();
move2 = new Move3();
move2.move();
class Move4 implements Move {
@Override
public void move() {
System.out.println("我在看电影2");
}
}
Move move4 = new Move4();
move4.move();
Move move = new Move() {
@Override
public void move() {
System.out.println("我在看电影3");
}
};
move.move();
Move move1 = () -> {
System.out.println("我在看电影3");
};
move1.move();
}
}
interface Move {
void move();
}
class Move2 implements Move {
@Override
public void move() {
System.out.println("我在看电影1");
}
}
4、线程状态
线程五大状态:创建、就绪、阻塞、运行以及死亡
操作线程的一些方法:
4.1、线程停止
(1)线程的停止建议使用正常停止 --> 利用次数来控制线程停止,但是不建议使用死循环。
(2)建议使用标志位来停止线程 --> 使用flag,来设置一个标志位。
(3)不要使用Stop或者destroy等过时或者JDK不建议使用的方法。
4.2、线程休眠
(1)Thread.sleep(毫秒)指定当前线程阻塞毫秒数;
(2)sleep时间达到后线程会进入就绪状态。
4.3、线程礼让
(1)礼让线程:让当前正在执行的线程暂停,但不阻塞;
(2)礼让不一定能成功;
(3)Thread.yield()。
4.4、Join线程
(1)Join线程,指的是合并线程,需要先执行此线程,将其他线程阻塞,可以想象成插队;
4.5、线程优先级
4.6、守护线程
现成分为用户线程和守护线程,Java虚拟机一定会确保用户线程执行完毕,但并不会保证守护线程执行完毕,这里面的守护线程有:后台记录操作日志、监控内存、垃圾回收等待等等。
5、线程同步
处理多线程问题时,多个对象访问同一个对象,并且某些线程还想修改这个对象时,就需要线程同步来避免数据紊乱;线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完之后,下一个线程使用。
5.1、线程不同步引起的三大不安全案例
(1)多线程买票案例
public class UnsafeBuyTicket {
public static void main(String[] args) {
BuyTicket buyTicket = new BuyTicket();
new Thread(buyTicket, "用户校长").start();
new Thread(buyTicket, "黄牛党").start();
new Thread(buyTicket, "VIP校长").start();
}
}
class BuyTicket implements Runnable {
private int ticketNum = 10;
private boolean flag = true;
@Override
public void run() {
while (flag) {
try {
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void buy() throws InterruptedException {
if (ticketNum <= 0) {
flag = false;
return;
}
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "-->购买到了第" + ticketNum-- + "张票");
}
}
(2)银行取钱问题
package ThreadDemo.syn;
/**
* @author : HaiLiang Huang
* @date : 2021年4月19日11:09:50
*/
public class UnsafeBank {
public static void main(String[] args) {
Account account = new Account(100,"结婚基金");
Drawing you = new Drawing(account,50,"我自己");
Drawing friend = new Drawing(account,100,"对象");
you.start();
friend.start();
}
}
class Account {
int money;
String name;
public Account(int money, String name) {
this.money = money;
this.name = name;
}
}
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() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (account.money - drawingMoney < 0) {
System.out.println("钱不够");
return;
}
account.money = account.money - drawingMoney;
nowMoney = nowMoney + drawingMoney;
System.out.println(account.name+"余额为:"+account.money);
System.out.println(this.getName()+"手里面有多少钱:"+nowMoney);
}
}
(3)list不安全问题
public class UnsafeList {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 100; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(list.size());
}
}
5.2、线程同步方法
为了解决上面的方案,引出三种实现线程同步的方法:
5.2.1、同步方法
只需要在可能会被修改数据的地方加上,比如上面的买票不安全问题,可以直接在买票的动作出直接加上synchronized
public synchronized void buy() throws InterruptedException {
if (ticketNum <= 0) {
flag = false;
return;
}
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "-->购买到了第" + ticketNum-- + "张票");
}
5.2.2、同步块
和上面的方法一样,只有在需要修改的地方加上synchronized
public void run() {
synchronized (account){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (account.money - drawingMoney < 0) {
System.out.println("钱不够");
return;
}
account.money = account.money - drawingMoney;
nowMoney = nowMoney + drawingMoney;
System.out.println(account.name+"余额为:"+account.money);
System.out.println(this.getName()+"手里面有多少钱:"+nowMoney);
}
}
5.2.3、死锁
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形,某一个同步块同时拥有"两个以上的锁"时,就可能发生死锁的问题。
1、产生死锁的四个条件:
(1)互斥条件
(2)请求与保持条件
(3)不剥夺条件
(4)循环等待条件
2、锁的使用方法:
6、线程协作
6.1、线程通信
线程之间存在相互依赖,互为条件,比如:生产者和消费者
(1)对于生产者,没有生产产品之前,需要通知消费者等待,而生产了产品之后还需要通知消费者启动;
(2)对于消费者,在消费之后,需要通知生产者生产出新的产品消费。
那么以上问题,仅仅使用线程同步方法是不够的,所以就有了线程之间的通信方法:
6.2、线程通信的解决方案
6.2.1、生产者消费者模式
在具体的实现中,生产者就只有生产的方法,消费就只有消费的方法,其中生产者与消费者之间的通信关系在缓存池中定义,比如容器满了,通知生产者等待;容器空了,通知消费者等待。
package ThreadDemo;
/**
* @author : HaiLiang Huang
* @date : 13:51
*/
// 生产者 消费者,产品 缓存区
public class PCTest {
public static void main(String[] args) {
SynContainer container = new SynContainer();
Productor productor = new Productor(container);
Consumer consumer = new Consumer(container);
productor.start();
consumer.start();
}
}
//生产者
class Productor extends Thread{
SynContainer container;
public Productor(SynContainer container){
this.container = container;
}
//生产
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
container.push(new Chicken(i));
System.out.println("生产了"+i+"只鸡");
}
}
}
//消费者
class Consumer extends Thread{
SynContainer container;
public Consumer(SynContainer container){
this.container = container;
}
//消费
@Override
public void run() {
for (int i = 1; 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;
//生产者放入产品
public synchronized void push(Chicken chicken){
//如果容器满了,就需要等待消费消费
while(count==chickens.length){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
chickens[count++] = chicken;
this.notifyAll();
}
//消费者消费产品
public synchronized Chicken pop(){
while (count == 0){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Chicken chicken = chickens[--count];
this.notifyAll();
return chicken;
}
}
6.2.2、信号灯模式
信号灯法,就是采用一个Boolean变量开控制生产者等待还是消费者等待:
public class PCTest2 {
public static void main(String[] args) {
TV tv = new TV();
Player player = new Player(tv);
Watcher watcher = new Watcher(tv);
player.start();
watcher.start();
}
}
// 演员
class Player extends Thread{
TV tv;
public Player(TV tv){
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
if (i%2==0){
this.tv.play("湖蓝卫视的快乐大本营");
}else {
this.tv.play("抖音:记录美好生活");
}
}
}
}
// 观众
class Watcher extends Thread{
TV tv;
public Watcher(TV tv){
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
this.tv.watch();
}
}
}
// 产品-->节目
class TV{
String voice;
boolean flag = true;
//如果在演员表演的时候,观众休息 T
public synchronized void play(String voice){
if (!flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.notifyAll();
this.voice=voice;
this.flag = !this.flag;
System.out.println("演员表演了"+voice);
}
//如果在观众观看的时候,演员休息 F
public synchronized void watch(){
if (flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.notifyAll();
System.out.println("观众观看了"+voice);
this.flag = !this.flag;
}
}
7、线程池
1、思路:考虑到经常创建和销毁线程会导致资源占用比较大,考虑可以提前创建好几个线程,放入线程池中,使用时直接获取,不适用的时候放回池中。
3、测试代码:
public class ThreadPoolsTest implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(5);
executorService.execute(new ThreadPoolsTest());
executorService.execute(new ThreadPoolsTest());
executorService.execute(new ThreadPoolsTest());
executorService.execute(new ThreadPoolsTest());
executorService.execute(new ThreadPoolsTest());
executorService.execute(new ThreadPoolsTest());
executorService.execute(new ThreadPoolsTest());
executorService.shutdown();
}
}