java 线程基础学习笔记

1. 线程基本概念

1.1 线程与进程

当一个程序进入内存运行时,即变成一个进程。进程是系统进行资源分配和调度的一个独立单元。

线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程。

归纳起来说就是:操作系统可以同时执行多个任务,每个任务就是进程;而进程也可以同时执行多个任务,每个任务就是线程。

1.2 并发与并行

并行:在同一时刻,有多条指令在多个处理器上同时执行

并发:在同一时刻,只能有一条指令执行,但多个指令在一个处理器上被快速轮流切换执行。

2. 线程的创建与启动

线程创建有4种方式,但是线程的启动只能调用线程对象的start() 方法

创建线程有以下4种方式:

  1. 继承 Thread 类
  2. 实现 Runnable 接口
  3. 实现 Callable 接口
  4. 线程池

2.1 继承Thread抽象类

public class Thread1 extends Thread {
    private int i;

    @Override
    public void run() {
        for (; i< 10; i++){
            System.out.println(getName()+ " " + i);
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName()+ " " + i);
            if (i == 10){
                new Thread1().start();
                new Thread1().start();
            }
        }
    }
}

2.2 实现Runnable接口

public class Demo{
    public static void main(String[] args) {
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName()+ " " + i);
            if (i == 10){
                //Lambda表达式实现runnable接口
                new Thread(()->{
                        for (int j= 0; j< 10; j++){
                            System.out.println(Thread.currentThread().getName()+ " " + j);
                        }
                }, "线程2").start();

                //Lambda表达式实现runnable接口
                new Thread(()-> {
                        for (int j = 0; j< 10; j++){
                            System.out.println(Thread.currentThread().getName()+ " " + j);
                        }
                }, "线程1").start();
            }
        }
    }
}

运行结果:

2.3 实现Callable接口

public class CallableDemo {

    public static void main(String[] args) {
		//使用FutureTask对象老包装 Callable 对象
      	//Callable 对象含有返回值
        FutureTask<Integer> task = new FutureTask<>(()-> {
                int i = 0;
                for( ; i< 10; i++){
                    System.out.println(Thread.currentThread().getName()+ " " + i);
                }
                return i;
        });
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName()+ " " + i);
            if (i == 10){
              	//start() 方法启动线程
                new Thread(task, "有返回值的线程").start();
            }
        }
        try {
          	//通过FutureTask 对象的get() 方法获取返回值
            System.out.println("子线程返回值:" + task.get());
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
}

运行结果:

先介绍以上三种,线程池后面细讲

2.4 三种方式对比

从实现方式上对比,实现 Runnable 接口与实现 Callable 接口属于同一种线程启动的方式。

采用继承Thread类创建线程的方式有以下优缺点:

  • 优势:编写简单,要获取当前线程不需要使用 Thread.currentThread() 方法,只需要使用 this 即可;
  • 劣势:java只能单继承,因为线程类继承了 Thread 类就不能再继承其他父类。

采用实现 Runnable、Callable 接口方式有以下优缺点:

  • 优势:实现接口后还能继承其他父类;
  • 优势:在这种方式下,多个线程可以共享同一个目标对象,所以非常适合多个线程来处理同一份资源的情况(也就是生产者消费者模式);
  • 劣势:变成稍稍复杂,要使用 Thread.currentThread() 方法获取当前线程。

3. 线程控制

3.1 线程生命周期

在线程的生命周期中,要经过新建(New)、就绪(Runnable)、运行(Run)、阻塞(Blocked)、和死亡(Dead)5种状态。尤其是当线程启动后,它不能一直霸占着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会在多次运行、阻塞之间切换。

3.2 join 线程

=> join(): 等待被join的线程执行完成;

=> join(long millis): 等待被join的线程时间最长为millis毫秒;

当在某个程序执行过程中调用其他线程的 join() 方法,调用线程将被阻塞,直到被 join() 方法加入的 join 线程执行完为止。

【例】在一道程序中有A线程和B线程两个,在B线程中调用 A.join(); 则程序会在A线程执行完毕后才会开始执行B线程。

线程睡眠:sleep (不释放所占有的资源)

static void sleep(long millis):让当前正在执行的线程暂停 millis 毫秒,并进入阻塞状态;

static vold sleep(long millis, int nanos):让当前正在执行的线程暂停 millis 毫秒加 nanos 毫微秒,并进入阻塞状态。

public class Thread1 extends Thread {

    private int i;

    @Override
    public void run() {
        for (; i< 10; i++){
            System.out.println(getName()+ " " + i);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName()+ " " + i);
            if (i == 5){
                Thread1 t1 = new Thread1();
                t1.start();
                //main 线程调用了t1 线程的join() 方法
                // 必须等t1 执行结束才会向下执行
                t1.join();
            }
        }
    }
}

运行结果:

3.3 线程睡眠:sleep

static void sleep(long millis):让当前正在执行的线程暂停 millis 毫秒,并进入阻塞状态;

static vold sleep(long millis, int nanos):让当前正在执行的线程暂停 millis 毫秒加 nanos 毫微秒,并进入阻塞状态。

【注】调用sleep() 方法不会释放所占有的资源

3.4 线程等待:wait

wait() 是Object 基类的方法,作用与sleep 类似,使得线程进入等待队列。不同的是,当线程调用wait()方法后会进入等待队列(进入这个状态会释放所占有的所有资源,与阻塞状态不同)

线程调用 wait() 方法后,要调用notify / notifyAll() 方法才能将其唤醒,进入就绪状态。

调用notify() 方法导致解除阻塞的线程是从因调用该对象的 wait()方法而阻塞的线程中随机选取的,我们无法预料哪一个线程将会被选择,所以编程时要特别小心,避免因这种不确定性而产生问题。

调用 notifyAll()方法将把因调用该对象的 wait()方法而阻塞的所有线程一次性全部解除阻塞 。当然,只有获得锁的那一个线程才能进入可执行状态。

3.5 线程让步:yield

yield() 方法是一个和sleep() 方法有点相似的方法,它也可是 Thread 类提供的一个静态方法,它可以让当前正在执行的线程暂停,但它不会阻塞该线程,它只是将该线程转入就绪状态。

yield() 只是让当前线程暂停一下,让系统的线程调度器重新调用一次,完全可能的情况是:当某个线程调用了yield() 方法暂停后,线程调度器又将其重新调度出来执行。

3.6 线程优先级:setPriority

每个线程执行时都具有一定的优先级,优先级高的线程会获得较多的执行机会。

每个线程默认的优先级都与创建它的父线程的优先级相同。(main 线程除外,默认情况下 main 线程创建的子线程和 main 线程都具有普通优先级)

Thread 类提供了 setPriority(int new Priority)、getPriority() 方法来设置和返回指定线程的优先级。范围的1~10之间。也可以使用 Thread 类的以下三个静态常量:

MAX_PRIORITY:其值为10。

MIN_PRIORITY:其值为1。

NORM_PRIORITY:其值为5。


4. 线程同步

解决线程安全问题的方式有:

  1. 同步代码块
  2. 同步方法
  3. 同步锁(Lock)

4.1 同步代码块

线程开始执行同步代码块之前,必须先获得同步监视器的锁定。语法格式:

synchronized(obj){
  ...
  //此处的代码为同步代码块
}

【注意】任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对同步监视器的锁定。

【提示】java 程序允许使用任意对象作为同步监视器,通常推荐使用可能被并发访问的共享资源充当同步监视器

4.2 同步方法

同步方法就是使用 synchronized 关键字来修饰某个方法。对于 synchronized 修饰的实例方法(非 static 方法)而言,同步方法的同步监视器就是 this,也就是调用该方法的对象。

JDK 所提供的 StringBuilder、StringBuffer 就是为了照顾单线程环境和多线程环境所提供的类。两个类代码几乎相似,只是 StringBuffer 的方法使用了synchronized 关键字修饰。

所以在单线程环境下应该使用 StringBuilder 来保证较好的性能;多线程环境应使用 StringBuffer 来保证线程安全。

4.3 同步锁(Lock)

同步锁是比 synchronized 方法和 synchronized 代码块更细粒度的同步机制,由 Lock 对象充当。

在线程安全控制中,比较常用的是 ReentrantLock (可重入锁)。使用该Lock 对象可显示地加锁、释放锁,通常使用 ReentrantLock 的代码格式如下:

class xxx {
  //定义锁对象
  private final ReentrantLock lock = new ReentrantLock();
  //...
  //定义需要保证线程安全的方法
  public void method(){
    //加锁
    lock.lock();
    try{
      //需保证线程安全的代码
      //... method body
    }finally{	//使用 finally 块来保证释放锁
      //释放锁
      lock.unlock;
    }
  }
}

5. 线程通信

5.1 传统的线程通信

传统的线程通信是借助于 Object 类提供的 wait()、notify()、notifyAll() 三个方法,结合 synchronized 修饰的同步方法或同步代码块实现的。在这里不做介绍。

5.2 Condition 控制线程

Java 提供了一个Condition 类,使用 Condition 可以让那些已经得到 Lock 对象却无法继续执行的线程释放 Lock对象,也可以唤醒 其他处于等待状态的线程。

Condition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用。

要获得特定 Lock 实例的Condition 实例,调用该 Lock 对象的 newCondition() 方法即可。

Condition 类提供了如下三个方法:

await():让当前线程进入等待状态,直到其他线程调用该 Condition 的 signal() 方法或 signalAll() 方法来唤醒该线程。

signal():唤醒在此 Condition 对象上等待的单个线程。如果有多个,则会随机唤醒一个。

signalAll():唤醒在此 Condition 对象上等待的所有线程。

例:经典问题:三个线程依次打印10次ABC,代码示例如下:

class Print {
    private Lock lock = new ReentrantLock();
    private Condition conditionA = lock.newCondition();
    private Condition conditionB = lock.newCondition();
    private Condition conditionC = lock.newCondition();
    private String type = "A"; //内部状态

    /*
     * 方法的基本要求为:
     * 1、该方法必须为原子的。
     * 2、当前状态必须满足条件。若不满足,则等待;满足,则执行业务代码。
     * 3、业务执行完毕后,修改状态,并唤醒指定条件下的线程。
     */
    public void printA() {
        lock.lock(); //锁,保证了线程安全。
        try {
            while (type != "A") { //type不为A,
                try {
                    conditionA.await(); //将当前线程阻塞于conditionA对象上,将被阻塞。
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            //type为A,则执行。
            System.out.println(Thread.currentThread().getName() + " 正在打印A");
            type = "B"; //将type设置为B。
            conditionB.signal(); //唤醒在等待conditionB对象上的一个线程。将信号传递出去。
        } finally {
            lock.unlock(); //解锁
        }
    }

    public void printB() {
        lock.lock(); //锁
        try {
            while (type != "B") { //type不为B,
                try {
                    conditionB.await(); //将当前线程阻塞于conditionB对象上,将被阻塞。
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            //type为B,则执行。
            System.out.println(Thread.currentThread().getName() + " 正在打印B");
            type = "C"; //将type设置为C。
            conditionC.signal(); //唤醒B
        } finally {
            lock.unlock(); //解锁
        }
    }

    public void printC() {
        lock.lock(); //锁
        try {
            while (type != "C") {
                try {
                    conditionC.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            System.out.println(Thread.currentThread().getName() + " 正在打印C");
            type = "A";
            conditionA.signal();    //唤醒A
        } finally {
            lock.unlock(); //解锁
        }
    }
}


public class Test{

    public static void main(String[] args) {
        final Business business = new Business();//业务对象。

        //线程1号,打印10次A。
        Thread ta = new Thread(()-> {

                for(int i=0;i<10;i++){
                    business.printA();
                }
        });

        //线程2号,打印10次B。
        Thread tb = new Thread(()-> {

                for(int i=0;i<10;i++){
                    business.printB();
                }
        });

        //线程3号,打印10次C。
        Thread tc = new Thread(()-> {

                for(int i=0;i<10;i++){
                    business.printC();
                }
        });

        //执行3条线程。
        ta.start();
        tb.start();
        tc.start();
    }
}

5.3 阻塞队列 BlockingQueue

Java 5 提供了一个 BlockingQueue 接口,作为线程同步的工具。

BlockingQueue 具有一个特征:当生产者线程试图向 BlockingQueue 中放入元素时,如果该队列已满,则该线程被阻塞;当消费者线程试图从 BlockingQueue 中取出元素时,如果该队列为空,则该线程被阻塞。

程序的两个线程通过交替向 BlockingQueue 中放入元素、取出元素,即可很好地控制线程的通信。

BlockingQueue 提供了如下两个支持阻塞的方法:

put(E e):尝试把 E 元素插入 BlockingQueue 尾部,如果该队列已满,则阻塞该线程。

take():尝试从 BlockingQueue 的头部取出元素,如果该队列的元素已空,则阻塞该线程。

代码例子:

public class BlockingQueueTest{
  public static void main(String[] args) throws Excption{
    // 定义一个长度为 2 的阻塞队列
    BlockingQueue<String> bq = new ArrayBlockingQueue<>(2);
    bq.put("Java");	// 将 "Java" 放入 bq
    bq.put("Spring"); // 将 "Spring" 放入 bq
    bq.put("JDBC");	// 阻塞线程(且 "JDBC" 不会被放入)
    Stirng str = bq.take(); // "Java" 被取出
  }
}

6. 线程池

系统启动一个新线程的成本是比较高的,因为涉及与操作系统交互。使用线程池可以很好地提高性能,尤其是当程序需要创建大量生存期很短的线程时,更应该考虑线程池。

与数据库连接池相类似,线程池在系统启动时即创建大量空闲的线程,程序将一个 Runnable 对象或 Callable 对象传给线程池,线程池就会启动一个线程来执行他们的 run() 或 call() 方法,当 run() 或 call() 方法执行结束后,该线程不会死亡,而是再次返回线程池中成为空闲状态。

6.1 四种常见的线程池

  1. Executors.newCacheThreadPool():可缓存线程池,先查看池中有没有以前建立的线程,如果有,就直接使用。如果没有,就建一个新的线程加入池中,缓存型池子通常用于执行一些生存期很短的异步型任务;
  2. **Executors.newFixedThreadPool(int n):**创建一个可重用的、固定线程数的线程池;
  3. **Executors.newSingleThreadExecutor():**创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
  4. **Executors.newScheduledThreadPool(int corePoolSize):**创建一个指定线程数的线程池,它可以在指定延迟时间间隔后再执行线程任务;

上面的前三个方法返回一个 ExecutorService 对象,该对象代表一个线程池,可以执行 Runnable 对象或 Callable 对象所代表的线程;而第 4 个方法返回一个 ScheduledExecutorService 对象,它是 ExecutorService 对象的子类,可以在指定延迟后执行线程任务。

前三个方法的代码例子:

public class ThreadPoolTest {
    public static void main(String[] args) {
        //创建一个可重用的、数量为6的线程池
        ExecutorService pool = Executors.newFixedThreadPool(6);
        // 创建一个可缓存线程池
//        ExecutorService pool = Executors.newCachedThreadPool();
        //创建一个只有单线程的线程池
//        ExecutorService pool = Executors.newSingleThreadExecutor();

        // Runnable 对象
        Runnable target = () -> {
            for (int i = 0; i < 21; i++) {
                System.out.println(Thread.currentThread().getName()+" " + i);
            }
        };

        // ExecutorService对象的 submit() 方法接收一个 Runnable 对象交给线程池,执行其 run() 方法;
        // 也可以接收 Callable 对象,执行其 call() 方法
        pool.submit(target);
        pool.submit(target);

        // 关闭线程池。会在全部线程任务执行完成后再关闭
        pool.shutdown();
    }
}

newScheduledThreadPool(int corePoolSize) 方法的代码例子:

public class ThreadPoolTest {
    public static void main(String[] args) {
        //创建一个数量为3的 ScheduledExecutorService 线程池
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(3);

      	// Runnable 对象
        Runnable target = () -> {
            for (int i = 0; i < 21; i++) {
                System.out.println(Thread.currentThread().getName()+" " + i);
            }
        };

        // ScheduledExecutorService 对象的 schedule() 方法可接收 Runnable 或 Callable 对象
        // 并指定延迟时间。以下代码会在 2 秒后再执行所以线程
        pool.schedule(target,1, TimeUnit.SECONDS);
      	pool.schedule(target,1, TimeUnit.SECONDS);

        // 关闭线程池。会在全部线程任务执行完成后再关闭
        pool.shutdown();
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值