前言

b站java课程学习笔记整理。

b站视频: 黑马程序员全套Java教程_Java基础入门视频教程,零基础小白自学Java必备教程

324. 进程和线程

线程是依赖于进程而存在。

进程:

  • 正在运行的应用程序
  • 是系统进行资源分配和调用的独立单位
  • 每一个进程都有它自己的内存空间和系统资源

线程:

  • 是进程中单个顺序控制流,是一条执行路径。
  • 单线程: 一个进程如果只有一条执行路径,则称为单线程程序(例如记事本设置时不能接着打字)
  • 多线程:一个进程如果有多条执行路径,则称为多线程程序(例如扫雷游戏进行时,计时器不受鼠标影响正常工作)

325. 继承Thread类的方式实现多线程

线程时程序中执行的线程。Java虚拟机允许应用程序同时执行多个线程。

多线程的实现方式:

方式1:继承Thread

  • 定义一个类MyThread继承hread
  • MyThread类中重写run()方法
  • 创建MyThread类的对象
  • 启动线程
package 线程;

public class MyThread extends Thread{
    @Override
    public void run() {
        for(int i = 0; i < 100; i++){
            System.out.println(i);
        }
    }
}

package 线程;

public class Demo{
    public static void main(String[] args) {
        MyThread m1 = new MyThread();
        MyThread m2 = new MyThread();


        // run方法的调用并没有启用线程。还是单线程按顺序执行。
        // m1.run();
        // m2.run();

        m1.start();
        m2.start();
    }
}

326. 设置和获取线程名称

Thread类中设置和获取线程名称的方法:

  • void setName(String name): 将线程的名称设置为name
  • String getName()返回此线程的名称

类要继承Thread

可以无参构造或者带参构造线程。Thread.currentThread()返回当前正在执行的的线程对象的引用。

package 线程;

public class MyThread extends Thread{

    public MyThread() {
    }

    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        for(int i = 0; i < 100; i++){
            System.out.println(getName()+","+ i);
        }
    }
}

package 线程;

public class SetGetDemo {
    MyThread m1 = new MyThread();
    MyThread m2 = new MyThread();


    public static void main(String[] args) {
        MyThread m1 = new MyThread();
        MyThread m2 = new MyThread();
        m1.setName("线程1");
        m2.setName("线程2");
        MyThread m3 = new MyThread("线程3");

        m1.start();
        m2.start();
        m3.start();

        System.out.println(Thread.currentThread().getName());  //main
    }
}

327. 线程优先级

线程有两种调度模型:

  • 分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用cpu的时间片
  • 抢占式调度模型: 优先让优先级高的线程使用cpu,如果线程的时间级相同,那么会随机选择一个,优先级高的线程获取的CPU时间片相对多一些。

Java使用的式抢占式调度模型。

label cpu在某一时刻只能执行一条指令,线程只有得到cpu时间片,也就是使用权,才可以执行指令。所以说相同优先级的多线程执行具有随机性。

设置和获取线程的优先级:

  • public final int getPriority(): 返回此线程的s优先级。
  • public final void setPriority(int newPriority): 更改此线程的优先级。
  • 线程范围的优先级最小值和最大值,默认值(可以设置的范围)为:MAX_PRIORITY = 10;MIN_PRIORITY = 1; ,NORM_PRIORITY = 5.

优先级高只是代表获取时间片的几率高,并不意味着每次都要跑到最前面。要在次数比较多时才能看到效果

328. 线程控制

方法名 说明
static void sleep(long millies) 使当前正在执行的线程停留(暂停执行)指定的毫秒数
void join() 等待这个线程死亡(必须等待这个线程执行完毕,别得线程才能执行)
void setDaemon(boolean on) 将此线程标记为守护线程,主线程执行完毕后,这些守护线程应该立即结束。
package 线程;

public class SetGetDemo {
    MyThread m1 = new MyThread();
    MyThread m2 = new MyThread();


    public static void main(String[] args){
        MyThread m1 = new MyThread();
        MyThread m2 = new MyThread();
        m1.setName("线程1");
        m2.setName("线程2");
        MyThread m3 = new MyThread("线程3");

        Thread.currentThread().setName("主线程");

        System.out.println(m1.getPriority());
        System.out.println(m2.getPriority());
        System.out.println(m3.getPriority());

        m1.start();
        try {
            m1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        m2.setDaemon(true);
        m3.setDaemon(true);

        m2.start();
        m3.start();


        for(int i = 0; i<10; i++){
            System.out.println(Thread.currentThread().getName()+","+i);

        }
    }
}

329. 线程的生命周期

线程生命周期

330. 实现Runnable接口的方式实现多线程

  1. 定义一个类MyRunnable实现Runnable接口。
  2. MyRunnable类中重写run()方法。
  3. 创建MyRunnable类的对象。
  4. 创建Thread类的对象,把MyRunnable对象作为构造方法的参数。
  5. 启动线程。
package Runnable实现多线程;

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for(int i = 0; i< 100; i++){
            //不能直接使用getname方法。
            System.out.println(Thread.currentThread().getName()+","+i);
        }
    }
}

package Runnable实现多线程;

public class Demo {
    public static void main(String[] args) {
        MyRunnable mr = new MyRunnable();

        Thread t1 = new Thread(mr,"高铁");
        Thread t2 = new Thread(mr,"飞机");

        t1.start();
        t2.start();
    }
}

这种方法并没有继承Thread类,这样他就可以有自己的父类了。

多线程的实现方案:

  • 继承Thread
  • 实现Runnable接口

Runnable的好处:

  • 避免了单继承的局限性。
  • 适合多个相同程序的代码去处理同一个资源的情况,把线程和程序的代码、数据有效分离,较好的体现了面向对象的设计思想。

331. 卖票

有一百张票,三个窗口同时实现卖票。

思路:

  1. 定义一个类SellTicket实现Runnable接口,里面定义一个成员变量:private int tickets = 100;

  2. SellTicket类中重写run()方法实现卖票,代码步骤如下:

    • 判断票数大于0,就卖票,并告知对应的卖票窗口。
    • 卖了票之后总数要减一。
    • 票没了,也可能有人来问。所以用死循环让卖票动作一直执行。
  3. 定义一个测试类SellTicketDemo,里面有main方法,代码步骤如下

    • 创建SellTicket类的对象
    • 创建三个Thread类的对象,把SellTicket对象作为构造方法的参数,并给出对应的窗口名称。
    • 启动线程
    package 卖票;
       
    public class SellTicket implements Runnable{
        private int tickets = 100;
       
        public int getTickets() {
            return tickets;
        }
       
        public void setTickets(int tickets) {
            this.tickets = tickets;
        }
       
        @Override
        public void run() {
       
            while(true){
                if(getTickets()>0){
                    System.out.println(Thread.currentThread().getName()+"正在出售第"+getTickets()+"张票");
                    tickets--;
                }
       
            }
        }
    }
       
    
    package 卖票;
       
    public class SellTicketDemo {
        public static void main(String[] args) {
            SellTicket sellTicket = new SellTicket();
       
            Thread t1 = new Thread(sellTicket,"售票窗口1");
            Thread t2 = new Thread(sellTicket,"售票窗口2");
            Thread t3 = new Thread(sellTicket,"售票窗口3");
       
       
            t1.start();
            t2.start();
            t3.start();
        }
    }
       
    

    332. 卖票案例的思考

    在出一张票的时候需要延迟100ms。

    出现三次卖第100张票的原因(相同的票出现了多次):

    1. t1线程抢到时间片,开始执行,然后休眠进入阻塞态。
    2. t2线程抢到时间片,开始执行,然后休眠进入阻塞态。
    3. t3线程抢到时间片,开始执行,然后休眠进入阻塞态。
    4. t1休息完了,抢到时间片,开始出第100张票。他还没开始做tickets--的时候
    5. t2休息完了,抢到时间片,开始出第100张票。他还没开始做tickets--的时候
    6. t3休息完了,抢到时间片,开始出第100张票。他还没开始做tickets--
    7. 执行了三次tickets--的操作,票数到了97。但是输出了三次100.

    出现卖第负数张票的原因:其实道理是一样的。(1, 0 , -1)。

    根本原因:线程执行的随机性。

    333. 同步代码块解决数据安全问题

    为什么出现问题?

    • 是否有多线程环境
    • 是否有共享数据
    • 是否有多条语句操作共享数据

    如何解决这个安全问题?

    • 破坏掉安全问题的环境(去除三个条件其一)

    怎么实现呢?

    • 把代码锁起来,让任意时刻只有一个进程进入。
    • java使用同步代码块
    • 格式
    synchronized(任意对象){
        多条语句操作的代码
    }
    

    synchronized(任意对象){}就相当于上锁了。

 package 卖票;
 
 public class SellTicket implements Runnable{
     private int tickets = 100;
 
     public int getTickets() {
         return tickets;
     }
 
     public void setTickets(int tickets) {
         this.tickets = tickets;
     }
 
     private  Object obj = new Object();
 
     @Override
     public void run() {
 
             while(true){
                 synchronized (obj) {
                     if (getTickets() > 0) {
                         try {
                             Thread.sleep(100);
                         } catch (InterruptedException e) {
                             e.printStackTrace();
                         }
                         System.out.println(Thread.currentThread().getName() + "正在出售第" + getTickets() + "张票");
                         tickets--;
                     }
                 }
 
             }
 
 
     }
 }
 
package 卖票;

public class SellTicketDemo {
    public static void main(String[] args) {
        SellTicket sellTicket = new SellTicket();

        Thread t1 = new Thread(sellTicket,"售票窗口1");
        Thread t2 = new Thread(sellTicket,"售票窗口2");
        Thread t3 = new Thread(sellTicket,"售票窗口3");


        t1.start();
        t2.start();
        t3.start();
    }
}

334. 同步方法解决数据安全问题

同步方法就是把synchronized关键字加到方法上。

  • 格式:
    • 修饰符synchronized返回值类型方法名(方法参数){}

同步方法锁的是this对象

同步静态方法:synchronized关键字加到静态方法上。

  • 格式:
    • 修饰符 static synchronized 返回值类型方法名 (方法参数){}

静态同步方法的锁是:

  • 类名.class

335. 线程安全的类

StringBuffer

  • 更安全
  • StringBuilder替代(但StringBuilder不是同步方法,不安全)

Vector

  • ArrayList代替Vector(但ArrayList不是同步方法,不安全)

Hashtable

  • HashMap可以代替,但同样,HashMap不安全。

多线程环境下使用:

Collections.synchronizedList(new ArrayList<String>() )包装成一个线程安全的类。

336. Lock锁

Lock实现提供比使用synchronized方法和语句可以更广泛的锁定操作。

Lock中提供了获得锁和释放锁的方法。

  • void lock(): 获得锁
  • void unlock(): 释放锁

Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化。

ReentrantLock的构造方法:

  • ReentrantLock(): 创建一个ReentrantLock的实例
package 卖票;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class SellTicket implements Runnable{
    private int tickets = 100;

    public int getTickets() {
        return tickets;
    }

    public void setTickets(int tickets) {
        this.tickets = tickets;
    }

    private Lock lock = new ReentrantLock();

    @Override
    public void run() {

        while(true){
            try {
                lock.lock();
                if (getTickets() > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在出售第" + getTickets() + "张票");
                    tickets--;
                }
            }finally {
                lock.unlock();
            }
            

        }
    }
}

一般使用try...finally的方法来在程序结束后释放锁。

337. 生产者消费者模式概述

生产者消费者模式:

  • 一类是生产者线程用于生产数据
  • 一类是消费者线程用于消费数据

为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库。

  • 生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为。
  • 消费者只需要从共享数据区中获取数据,并不需要关心生产者的行为。

为了体现等待和唤醒问题:

方法名 说明
void wait() 导致当前线程等待,直到另一个线程调用该对象的notify()方法或notifyAll()方法
void nofity() 唤醒正在等待对象监视器的单个线程
void notifyAll() 唤醒正在等待对象监视器的所有线程

338. 生产者消费者案例

模拟牛奶工送奶。

package 生产者和消费者;

public class Box {
    private int milk;
    private boolean state = false;

    public synchronized void put(int milk){
        if(state){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.milk = milk;
        System.out.println("送奶工将第"+this.milk+"瓶奶放入奶箱");
        state = true;
        notifyAll();



    }

    public synchronized void get(){
        if(!state){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("用户拿到第"+this.milk+"瓶奶");
        state = false;
        notifyAll();
    }
}

package 生产者和消费者;

public class Producer implements Runnable{
    private Box b;
    public Producer(Box b) {
        this.b = b;
    }

    @Override
    public void run() {
        for(int i= 1;i<=5;i++){
            b.put(i);
        }
    }
}

package 生产者和消费者;

public class Customer implements Runnable{
    private Box b;
    public Customer(Box b) {
        this.b = b;
    }

    @Override
    public void run() {
        while(true){
            b.get();
        }
    }
}

package 生产者和消费者;

public class Demo {
    public static void main(String[] args) {
        Box b = new Box();

        Producer producer = new Producer(b);
        Customer customer = new Customer(b);

        Thread t1 = new Thread(producer);
        Thread t2 = new Thread(customer);

        t1.start();
        t2.start();
    }

}