java多线程基础学习(java多线程 菜鸟教程)

  本篇文章为你整理了java多线程基础学习(java多线程 菜鸟教程)的详细内容,包含有java多线程实战指南 java多线程 菜鸟教程 java多线程有几种实现方法,都是什么 java多线程使用方法 java多线程基础学习,希望能帮助你了解 java多线程基础学习。

  效果展示

  可以看见子线程和主线程交替执行,但同一时间只能由一个执行,即宏观并发,微观交替

  
需要用到common-io包

  实现WebDownLoader类,里面实现一个DownLoader()方法,主要用到FileUtils.copyURLToFile()方法,将URL资源转为图片,即实现下载

  主类继承Thread类,run()方法调用DownLoader()来实现多线程下载

  main方法创建对象,调用start()方法开启线程

  
// 创建多线程对象

   TestThreadToDownload t1 = new TestThreadToDownload("https://img1.baidu.com/it/u=3726701668,178087506 fm=253 fmt=auto app=138 f=JPEG?w=353 h=499","./src/com/kuang/class1/image/1.jpg");

   TestThreadToDownload t2 = new TestThreadToDownload("https://img1.baidu.com/it/u=3726701668,178087506 fm=253 fmt=auto app=138 f=JPEG?w=353 h=450","./src/com/kuang/class1/image/2.jpg");

   TestThreadToDownload t3 = new TestThreadToDownload("https://img1.baidu.com/it/u=3726701668,178087506 fm=253 fmt=auto app=138 f=JPEG?w=353 h=500","./src/com/kuang/class1/image/3.jpg");

   // 开启线程

   t1.start();

   t2.start();

   t3.start();

  
// 主要用到该方法,将传入的URL下载成本地文件

   FileUtils.copyURLToFile(new URL(url),new File(name));

   catch(IOException e){

   e.printStackTrace();

   System.out.println("IO异常,DownLoader下载出问题");

  

 

 

  
实现Runnable接口并且必须重写其中的run()方法

  Runnable创建多线程使用静态代理模式,首先必须创建实现了该接口的类的对象

  然后将对象传入Thread的构造器,创建一个Thread对象

  最后通过Thread类的对象调用start()方法开启线程

  
Runnable接口实现多线程比较特殊,但最终都是通过Thread类来运行的;推荐使用Runnable接口而非Thread类,因为java只能单继承,但可以实现多个接口

  
// 先创建实现了Runnable接口的对象

   // 再将这个对象传入Thread的构造器,创建一个Thread类对象

   // 最后调用start方法开启

   new Thread(new TestRunnableDemo()).start();

   // 主线程代码

   for (int i = 0; i i++) {

   System.out.println("主线程--" +i);

  

 

 

  
设置一个静态类变量winner模拟胜出者

  设置一个裁判方法,如果已经存在胜出者,则线程停止;如果步数大于等于100,将该线程名赋给winner,否则线程继续循环

  
// 兔子在中间开始休息

   if (i % 45 == 0 Thread.currentThread().getName().equals("兔子")) {

   try {

   Thread.sleep(300);

   } catch (InterruptedException e) {

   throw new RuntimeException(e);

   // 模拟乌龟跑的慢

   if(Thread.currentThread().getName().equals("乌龟")) {

   try {

   Thread.sleep(5);

   } catch (InterruptedException e) {

   throw new RuntimeException(e);

   // 裁判方法

   public boolean gameOver(int steps) {

   if(winner != null) {

   return true;

   if (steps = 100) {

   winner = Thread.currentThread().getName();

   System.out.println("胜出者是" + winner);

   return true;

   return false;

   public static void main(String[] args) {

   TestRaceDemo raceDemo = new TestRaceDemo();

   new Thread(raceDemo,"兔子").start();

   new Thread(raceDemo,"乌龟").start();

  

 

 

  
实现Callable接口,并且重写call()方法,该方法具有返回值,返回值类型同implements定义Callable [返回值类型] 时的类型

  主方法开启多线程由两种方法:

  方法1:分为创建服务、提交执行、获取结果、关闭服务四个固定步骤

  第0步,先创建实现了Callable接口的类的对象

  通过ExecutorService service = Executors.newFixedThreadPool( 线程池容量 )创建一个自定义线程数的线程池,并开启服务

  Future [call方法返回类型] r1 = service.submit( 第0步对象 )将线程提交执行

  r1.get()获取线程执行完毕后的返回值

  service.shutdownNow()关闭服务

  方法2:通过Thread代理模式开启线程

  先创建实现了Callable接口的类的对象

  FutureTask String task = new FutureTask (call)创建FutureTask对象

  new Thread(task,"小明").start()开启线程

  FutureTask泛型间接实现了Runnable接口,相当于转换了一下

  
public static void main(String[] args) throws ExecutionException, InterruptedException {

   // 创建目标对象

   TestCallableDemo call = new TestCallableDemo();

   // 创建服务,这里创建了一个容纳三个线程的线程池

   ExecutorService service = Executors.newFixedThreadPool(3);

   // 提交执行

   Future String r1 = service.submit(call);

   Future String r2 = service.submit(call);

   Future String r3 = service.submit(call);

   System.out.println(r1.get());

   System.out.println(r2.get());

   System.out.println(r3.get());

   service.shutdownNow();

  
创建票数的变量

  实现Runnable接口,在run()方法中模拟买票,每执行一次(买一张票),票就减少一张

  主线程创建多个子线程来模拟并发

  
public class TestBuyTicketDemo implements Runnable {

   private static int ticketNum = 10; // 票的数量

   @Override

   public void run() {

   while (ticketNum 0) {

   // 模拟延时

   try {

   Thread.sleep(50);

   } catch (InterruptedException e) {

   throw new RuntimeException(e);

   // 买票程序

   System.out.println(Thread.currentThread().getName() + "拿到了第" + ticketNum-- + "票");

   public static void main(String[] args) {

   TestBuyTicketDemo t = new TestBuyTicketDemo();

   // 多线程买票

   new Thread(t,"小明").start();

   new Thread(t,"张三").start();

   new Thread(t,"黄牛党").start();

  

 

 

  
避免匿名内部类定义过多,可以让代码简洁紧凑,留下核心的逻辑

  Lambda表达式能够让程序员的编程更加高效

  
前提:必须存在一个函数式接口(即接口中只定义了一个抽象方法),这样我们就可以使用Lambda表达式来创建该接口的对象

  
2.5.2、Lambda表达式用法引入

  将接口的实现类、静态内部类、局部内部类、匿名内部类、Lambda式进行对比,可以发现Lambda表达式异常简洁

  
效果展示

  定义接口对象like5,使用Lambda表达式,只关心核心逻辑的写法;使用时,直接通过该对象调用接口中的方法即可

  
// 这里使用lambda表达式,实现了接口中的抽象方法,将两数运算具体化,这里定义加法运算

   MathOperation add = Integer::sum;

   // 减法运算

   MathOperation sub = (int a, int b) - a - b;

   // 乘法运算

   MathOperation multi = (a,b) - a * b;

   System.out.println(add.operation(1,2));

   System.out.println(sub.operation(3,5));

   System.out.println(multi.operation(8,7));

   // 定义函数式接口,

   interface MathOperation {

   // 将两数运算抽象出来

   int operation(int a, int b);

  

 

 

  
三、线程状态转换

  线程的五大状态在概述中已经讲到,这里主要讲解让线程状态转换的方法

  3.1、线程停止

  不推荐使用jdk中的内置方法,如stop(),因为这是使线程强制停止的方法,可能会导致多线程的一些问题

  推荐自定义标志位,并加上逻辑判断,让线程在某一条件下自动停止运行

  
while (i != 15) { // 设置标志位,让线程主动停止

   System.out.println("子线程---run" + i++);

   System.out.println("子线程停止!!!");

   public static void main(String[] args) {

  
public static void main(String[] args) {

   Date date = new Date(System.currentTimeMillis()); // 获得当前时间

   while(true) {

   try {

   Thread.sleep(1000); // 一秒休眠一次

   System.out.println(new SimpleDateFormat("HH:mm:ss").format(date));

   date = new Date(System.currentTimeMillis()); // 更新时间

   catch (InterruptedException e) {

   e.printStackTrace();

  

 

 

  
3.3、线程礼让

  主要用到yield()方法,在多线程情况下,让正处在运行态的线程转换为就绪态,重新竞争CPU调度(但是礼让不一定成功,再次调度还是看CPU心情)

  
3.4、线程加入

  主要用到jion()方法,线程加入相当于该线程插队,插队后该线程强制执行到结束,其他线程在此期间阻塞

  
public static void main(String[] args) throws InterruptedException {

   TestThreadJoin join = new TestThreadJoin();

   Thread thread = new Thread(join);

   thread.start();

   for (int i = 0; i i++) {

   if(i == 5) {

   thread.join(); // 线程插队,并且强制执行

   System.out.println("main线程" + i);

  

 

 

  
public static void main(String[] args) throws InterruptedException {

   Thread thread = new Thread(() - {

   for (int i = 0; i i++) {

   if (i % 2 == 0) {

   try {

   Thread.sleep(1000);

   } catch (InterruptedException e) {

   throw new RuntimeException(e);

   System.out.println("线程运行中。。。");

   System.out.println("/////////");

   Thread.State state = thread.getState();

   System.out.println(state); // NEW状态

   thread.start();

   state = thread.getState();

   System.out.println(state);

   while(state != Thread.State.TERMINATED) {

   state = thread.getState();

   System.out.println(state);

   Thread.sleep(500); // 每0.5s检测一次状态

  

 

 

  
线程的优先级从低到高分为了10个等级,分别对应数字1到10

  一条线程可以使用setPriority()方法自定义设置优先级,可以使用getPriority()查看优先级

  高优先级并非优先调度,只是被调度的概率提高了,具体调度还是得看CPU

  
public void run() {

   System.out.println(Thread.currentThread().getName() + "的优先级是" + Thread.currentThread().getPriority());

   public static void main(String[] args) {

   TestThreadPriority test = new TestThreadPriority();

   Thread t1 = new Thread(test,"t1");

   Thread t2 = new Thread(test,"t2");

   Thread t3 = new Thread(test,"t3");

   Thread t4 = new Thread(test,"t4");

   Thread t5 = new Thread(test,"t5");

   // 线程自定义优先级

   t1.setPriority(Thread.MAX_PRIORITY);

   t2.setPriority(Thread.MIN_PRIORITY);

   t3.setPriority(8);

   t4.setPriority(3);

   t5.setPriority(Thread.NORM_PRIORITY); // 默认优先级

   t1.start();

   t2.start();

   t3.start();

   t4.start();

   t5.start();

  

 

 

  
java提供了两种线程:守护线程和用户线程

  守护线程,是指在程序运行时 在后台提供一种通用服务的线程,这种线程并不属于程序中不可或缺的部分

  用户线程可以理解为被守护线程,JVM会等待所有用户线程,当用户线程执行完毕后,只剩守护线程存在,这时JVM会关闭,而不会等待守护线程再执行完毕,会杀死所有的守护线程(因为守护线程守护着用户线程,当用户线程执行完毕后,守护线程就无事可做了,当然没必要再往下继续执行了)

  守护线程的优先级一般较低,用户可以使用setDaemon()方法主动设置线程为守护线程

  
// 这句话永远不会输出,因为被守护线程关闭后,jvm就关闭了,jvm不会等守护线程结束

   System.out.println(Thread.currentThread().getName()+"线程关闭");

   public static void main(String[] args){

   System.out.println("main线程启动");

   TestDaemon test = new TestDaemon();

   Thread thread = new Thread(test,"A");

  
效果展示

  可以发现JVM不会等待守护线程执行完毕,在用户线程执行完后会立即杀死守护线程

  
四、线程同步

  ​ 第二节讲到不控制线程并发的一些问题,比如买票是抢到同一张票(数据不唯一),严重的可能会导致死锁现象,即线程的永久等待;本节针对控制多线程并发的一系列问题,提出相应的解决方法。

  4.1、线程同步的概念

  
线程不同步的后果

  ​ 当多线程在操作共享资源时,如果不控制线程的并发,就可能会导致共享区数据不唯一,或者线程间的死锁现象;尤其针对高并发环境下,比如12306买票,或者微信抢红包;如果不控制并发,很可能会出现比如有人在群里发了一个一百块红包,一百人同时开抢,可能这一百人都会抢到一百块钱,给平台造成损失。

  
何时需要线程同步

  ​ 当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态,实现线程同步的方法有很多,临界区对象(共享资源)就是其中一种

  线程同步的底层涉及到一些算法问题,深入理解的话就需要学习《操作系统》

  
①、synchronized修饰方法

  ​ 使方法转变为同步方法,相当于给方法加锁;当方法中有线程正在操作,该方法就会被加锁,其他线程就必须排队;直到该线程操作完毕后主动解锁,其他线程再竞争进入该方法;之后依旧进行加锁和解锁操作。

  
重现抢票程序,观察使用synchronized修饰前和修饰后,输出结果的变化

  

package com.kuang.class4.synchronizedDemo;

 

   * 不安全的买票案例,体验同步修饰符的用法

  public class UnsafeBuyTicket {

   public static void main(String[] args) {

   BuyTicket station = new BuyTicket();

   new Thread(station,"小明").start();

   new Thread(station,"黄牛党").start();

  class BuyTicket implements Runnable {

   private int ticketNum = 10; // 票的数量

   boolean flag = true;

   @Override

   public void run() {

   while(flag) {

   try {

   buy();

   } catch (InterruptedException e) {

   throw new RuntimeException(e);

   // 此处将synchronized去掉再加上,观察输出变化

   private synchronized void buy() throws InterruptedException {

   if (ticketNum = 0) {

   flag = false;

   return;

   System.out.println(Thread.currentThread().getName() + "拿到了第" + ticketNum-- + "张票");

   Thread.sleep(100);

  

 

  给buy()方法加锁,观察变化

  


 

 

  此处obj称为同步监视器,obj可以是任何对象,但一般选用共享区资源作为监视器,例如上述买票程序中的票的数量ticketNum就是共享区资源

  
多线程操作不安全的泛型集合ArrayList,观察加锁前后变化

  

package com.kuang.class4.synchronizedDemo;

 

  import java.util.ArrayList;

  import java.util.List;

   * 线程不安全的泛型集合,体验同步块的用法

  public class UnsafeList {

   public static void main(String[] args) {

   List String list = new ArrayList ();

   for (int i = 0; i 100000; i++) {

   new Thread(() - {

   synchronized (list) {

   list.add(Thread.currentThread().getName());}

   }).start();

   // 这里的sleep是为了让主线程休眠,否则主线程可能会提前输出list的size

   try {

   Thread.sleep(3000);

   catch (InterruptedException e) {

   e.printStackTrace();

   System.out.println(list.size());

  

 

  多线程给list添加1000000条数据,最后输出list的大小

  
4.2.2、Lock锁

  取自JUC包下的锁机制,只能对代码块进行加锁,但效率比synchronized更高

  ①、一般写法

  

private final ReentrantLock lock = new ReentrantLock(); // 定义锁

 

  public void run() {

   try {

   lock.lock();

   // 这里写需要同步的代码块

   finally {

   lock.unlock(); // 如同步代码块有异常,则将解锁写在此处

   // 但一般都在此处解锁

  

 

  ②、案例说明

  继续以买票案例为例

  
System.out.println(Thread.currentThread().getName() + "拿到了第" + ticketNum-- + "张票");

   finally {

   lock.unlock(); // 退出临界区后解锁

   Thread.sleep(1000); // 本线程阻塞,其他线程开始竞争

  

 

 

  
2.1.1、为什么需要线程通信

  线程是操作系统调度的最小单位,有自己的栈空间,可以按照既定的代码逐步的执行,但是如果每个线程间都孤立的运行,那就会造资源浪费。所以在现实中,我们需要这些线程间可以按照指定的规则共同完成一件任务,所以这些线程之间就需要互相协调,这个过程被称为线程的通信

  因此线程通信可以概括为:当多个线程共同操作共享的资源时,互相告知自己的状态以避免资源争夺,并且互相协调,完成同一件任务

  2.1.2、线程的通信的方式

  2.2、管程法

  实现生产者、消费者模型,该模型大致定义如下:

  存在缓冲区,生产者在缓冲区放入东西,消费者从缓冲区取出东西进行消费

  生产者,当缓冲区不满时,生产者就生产东西放入缓冲区;当缓冲区满时,生产者就停止生产进入阻塞,并通知消费者取出东西

  消费者,当缓冲区存在东西时,就不断取出;当缓冲区为空时,消费者停止消费进入阻塞,并通知生产了生产东西

  
new Producer(container).start(); // 启动生产者线程

   new Customer(container).start(); // 启动消费者线程

  
container.push(new Goods(i)); // 给缓冲区放入东西

   System.out.println("生产了第" + i + "个商品");

  class Customer extends Thread {

   SynContainer container;

   public Customer(SynContainer container) {

   this.container = container;

   @Override

   public void run() {

   for (int i = 1; i 101; i++) {

   System.out.println("消费了第" + container.pop().id + "个商品");

   // 从缓冲区取出东西

  // 商品类

  class Goods {

   int id; // 产品编号

   public Goods(int id) {

   this.id = id;

  // 缓冲区类

  class SynContainer {

   Goods[] goods = new Goods[10]; // 定义一个容量为10的缓冲区

   int count = 0; // 缓冲区商品计数器

   // 放入缓冲区

   public synchronized void push(Goods g) {

   if (count == goods.length) {

   // 缓冲区满了,生产者休息

   try {

   this.wait();

   catch (InterruptedException e) {

   e.printStackTrace();

   goods[count] = g;

   count++;

   // 通知消费者消费

   this.notifyAll();

   // 从缓冲区取出

   public synchronized Goods pop() {

   if (count == 0) {

   try {

   // 缓冲区为空,消费者阻塞

   this.wait();

   catch (InterruptedException e) {

   e.printStackTrace();

   count--;

   // 通知生产者生产

   this.notifyAll();

   return goods[count];

  

 

 

  
2.3、信号量法

  是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。

  
代码实现

  代码实现生产者消费者模型的吃水果案例,妈妈往盘子中放水果,我从盘子中拿水果吃,盘子只有一个;当盘子中有水果,就不能再放了,并通知我吃水果;当盘子中没了水果,我就不能吃,并通知妈妈放水果。

  

package com.kuang.class5;

 

   * 吃水果的生产者消费者模型

   * 妈妈往盘子放水果,我从盘子中取水果吃

  public class TestEatFruit {

   public static void main(String[] args) {

   Plate plate = new Plate();

   new Me(plate).start();

   new Mom(plate).start();

  class Mom extends Thread {

   Plate plate;

   public Mom(Plate plate) {

   this.plate = plate;

   @Override

   public void run() {

   for (int i = 0; i i++) {

   if (i % 2 == 0) {

   plate.put("苹果");

   else if (i % 5 == 0) {

   plate.put("香蕉");

   else {

   plate.put("橘子");

  class Me extends Thread {

   Plate plate;

   public Me(Plate plate) {

   this.plate = plate;

   @Override

   public void run() {

   for (int i = 0; i i++) {

   plate.eat();

  class Plate {

   String fruitName; // 水果名

   boolean mutex = true; // 定义互斥信号量

   public synchronized void put(String name) {

   if (!mutex) {

   try {

   this.wait(); // 当盘子中有水果就必须阻塞

   catch (InterruptedException e) {

   e.printStackTrace();

   System.out.println("妈咪给了一个" + name);

   this.notifyAll(); // 放了一个水果,通知我吃水果

   this.fruitName = name;

   this.mutex = !mutex;

   public synchronized void eat() {

   if (mutex) {

   try {

   this.wait(); // 当盘子中没有水果,就阻塞

   catch (InterruptedException e) {

   e.printStackTrace();

   System.out.println("我吃掉了一个" + fruitName);

   this.notifyAll(); // 吃掉一个水果,通知放水果

   this.mutex = !mutex;

  

 

  
2.4、线程池法

  在高并发情况下,经常要进行线程的创建与销毁,对性能影响很大;线程池法的思路就是提前创建好多个线程,放入线程池中,使用时直接获取,使用完后放入池中,可以必变重复的创建和销毁操作,提高服务器效率(可以类比共享单车,提前投放一批单车,使用时直接扫码用,使用完后放回单车点)

  思路:

  多线程实现类实现Runnable接口

  主类使用ExecutorService创建线程池

  使用execute(Runnable obj)执行任务,该方法属于上面接口,没有返回值

  最后使用shutdown(),关闭连接池

  
// 该部分内容属于JUC编程,Executors也属于JUC包

   ExecutorService service = Executors.newFixedThreadPool(10);

   service.execute(new MyThread());

   service.execute(new MyThread());

   service.execute(new MyThread());

   service.execute(new MyThread());

   service.shutdown();

  class MyThread implements Runnable {

   @Override

   public void run() {

   System.out.println(Thread.currentThread().getName());

  

 

 

  以上就是java多线程基础学习(java多线程 菜鸟教程)的详细内容,想要了解更多 java多线程基础学习的内容,请持续关注盛行IT软件开发工作室。

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: