多线程基础知识点汇总

线程依赖于进程而存在。

进程

  • 通过任务管理器看到进程的存在。
  • 只有运行的程序才会出现进程。
  • 就是正在运行的程序。是系统进行资源分配和调用的独立单位
  • 每一个进程都有它自己的

内存空间和系统资源。

多进程有什么意义?

单进程的计算机只能做一件事情,现在的计算机可以做多件事情。
比如:一边玩游戏(游戏进程),一边听音乐(音乐进程)。
现在的计算机都支持多进程,可以在一个时间段内执行多个任务,提高CPU的使用率。
问题:一边玩游戏一边听音乐是同时进行的吗?
不是,单核CPU在某一时间点上只能做一件事情。只是CPU做着程序间的高效切换让我们觉得是同时进行的。

线程

  • 在同一个进程内又可以执行多个任务。每一个任务是一个线程。
  • 线程是程序的执行单元,执行路径。是程序使用CPU的最基本单位
  • 单线程:程序只有一条执行路径。
  • 多线程:程序有多条执行路径。

多线程有什么意义?

多线程的存在,不是提高程序的执行速度,而是为了提高应用程序的使用率。
程序的执行其实都是在抢CPU的资源,CPU的执行权。
多个进程实在抢这个资源,某一个进程如果执行路径比较多,就会有更高的几率抢到CPU的执行权。
不敢保证哪个线程能够在哪个时刻抢到,所以线程的执行有随机性
比如:扫雷程序(一个计时器,一个鼠标点击);迅雷下载

线程两种调度模型

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

使用t1.getPriority()查看优先级
使用t1.setPriority(…)设置优先级

知识点
休眠线程(隔一段时间运行一次,sleep);
加入线程(运行完该线程才去运行其他线程,join);
礼让线程(使得运行次序尽量交替进行,yield)
守护线程(当一个线程结束之后,其他线程也将不在运行,典型例子:坦克大战 setDaemon)
中断线程(超过一段时间可以将其停止,interrupt)

线程的生命周期:(记住步骤)
新建:创建线程对象
就绪:有执行资格,没有执行权
运行:有执行资格,有执行权
(阻塞):由于一些操作让线程处于该状态,没有执行资格,没有执行权,另一些操作可以把它激活,激活后处于就绪状态。
死亡:线程对象变成垃圾,等待被回收

判断一个程序是否会有线程安全问题的标准
1.是否有多线程环境
2.是否有共享数据
3.是否有多条语句操作共享数据**
解决方法:因为12点改变不了,我们只能对3这一点进行修改
法1:同步代码块,对象obj作为一把锁的功能。

private Object obj = new Object();
public void run() {
            synchronized (obj) {  //也可用this,如果是静态的方法,则是类的字节码文件对象,
            ......
            }
        }

法2:把同步关键字加在方法上。

public void run() {
            sellTicket();
        }
private (static) synchronized void sellTicket(){ //默认锁对象是this,静态时默认为类.class
......
}

法3:Lock接口,ReentrantLock是Lock的实现类。

private Lock lock = new ReentrantLock();
public void run(){
    ...
    try{
        lock.lock();
        //需要加锁的代码
        ...
    }finally{
        lock.unlock();
    }
}

并发、并行、同步

  • 并发:逻辑上同时发生,指在某一时间段内同时运行多个程序。
  • 并行:物理上同时发生,指在某一时间点同时运行多个程序。
  • 同步:特点:多个线程使用的是同一个锁对象。同步的出现解决了多线程的安全问题。当线程相当多时,因为每个线程都会判断同步上的锁,这是很耗费资源的,无形中降低程序的运行效率。

Java程序的运行原理
由java命令启动JVM,JVM启动相当于启动了一个进程。
由该进程创建一个主线程去调用main方法。
JVM虚拟机启动是多线程的。原因是垃圾回收线程也要先启动,否则很容易出现内存溢出。

如何实现多线程
由于线程是依赖进程而存在的,所以我们应该先创建一个进程出来。
而进程是由系统创建的,所以我们应该去调用系统功能创建一个进程。
Java不能直接调用系统功能,所以没有办法直接实现多线程程序。
但是,java可以调用C/C+ +写好的程序来实现多线程程序。
由C/C++去调用系统功能创建进程,然后由Java提供一些类(Tread)去调用线程,就实现了多线程程序。

查看API,发现有两种方式实现多线程程序。
方式一:继承Thread类
重写run方法,用来包含那些被线程执行的代码。
run方法的调用其实就是普通方法的调用,看到的是单线程的效果。
start方法,首先启动了线程,然后再由jvm去调用该线程的run方法。

package thread;
public class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 200; i++) {
            System.out.println(getName() + "---" + i);
        }
    }
}
/////////////////////////////////////////
package thread;
public class ThreadDemo {
    public static void main(String[] args) {
        MyThread m1 = new MyThread();
        MyThread m2 = new MyThread();
        m1.start();
        m2.start();
    }
}

结果是两个线程抢资源,200个数字交替打印出。

方式二:实现Runnable接口(常用)
1. 自定义类MyRunnable实现Runnable接口
2. 重写run方法
3. 创建MyRunnable类的对象
4. 创建Thread类的对象,并把3步骤的对象作为构造参数传递

package thread;
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}
////////////////////////////////////////////////
package thread;
public class MyRunnableDemo {
    public static void main(String[] args) {
        MyRunnable my = new MyRunnable();
        Thread t1 = new Thread(my);
        Thread t2 = new Thread(my);
        t1.start();
        t2.start();
    }
}

方式三:实现Callable接口
需要用线程池配合

方式四:匿名内部类方法创建并开启线程

public class Demo {
    public static void main(String[] args) {
        new Thread() {
            // 重写run方法
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println(Thread.currentThread().getName() + ":"+ i);
                }
            };
        }.start();
        // 
        new Thread(new Runnable() {

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

死锁问题

同步弊端:效率低,如果出现了同步嵌套,就容易产生死锁问题。
死锁:是指两个或两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待现象。

线程间通信 生产者消费者问题
不同种类的线程间针对同一个资源的操作。
例子:学生
资源类:Student
设置学生数据:SetThread(生产者)
获取学生数据:GetThread(消费者)
测试类:StudentDemo
正常思路:
A:生产者 先看是否有数据,有就等待,没有就生产,生产完之后通知消费者来消费
B:消费者 先看是否有数据,有就消费,没有就等待。通知生产者生产数据
为了处理这样的问题,Java就提供了一种机制,等待唤醒机制
等待唤醒:
Object类中提供了三个方法:
wait():等待
notify():唤醒单个线程
notifyAll():唤醒所有线程
这些方法的调用必须通过锁对象调用,而使用的锁对象是任意锁对象。所以,必须定义在Object类中。
*注:
1wait之后就释放锁,将来唤醒的时候从这里开始执行
2唤醒并不表示有执行权,必须还是去抢执行权*
例子代码如下:

//Student类/
public class Student {
    String name;
    int age;
    boolean flag;// 默认情况下没有数据,如果为true,说明有数据
}
//设置学生数据线程类//
public class SetThread implements Runnable {
    private Student s;
    private int x = 0;

    public SetThread(Student s) {
        this.s = s;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (s) {
                // 判断有没有数据
                if (s.flag) {
                    try {
                        s.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                if (x % 2 == 0) {
                    s.name = "james";
                    s.age = 32;
                } else {
                    s.name = "kobe";
                    s.age = 37;
                }
                x++;
                // 修改标记
                s.flag = true;
                // 唤醒线程
                s.notify();
            }
        }
    }
}
//读取学生数据线程类
public class GetThread implements Runnable {
    private Student s;

    public GetThread(Student s) {
        this.s = s;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (s) {
                if (!s.flag) {
                    try {
                        s.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(s.name + ":" + s.age);
                // 修改标记
                s.flag = false;
                // 唤醒线程
                s.notify();
            }
        }
    }
}
//测试结果类///
public class StudentDemo {
    public static void main(String[] args) {
        Student s = new Student();
        SetThread st = new SetThread(s);
        GetThread gt = new GetThread(s);
        Thread t1 = new Thread(st);
        Thread t2 = new Thread(gt);
        t2.start();
        t1.start();
    }
}

新的线程生命周期图如下:
多线程生命周期
常见的情况:
A:新建–就绪–运行–死亡
B:新建–就绪–运行–就绪–运行–死亡
C:新建–就绪–运行–其他阻塞–就绪–运行–死亡
D:新建–就绪–运行–同步阻塞–就绪–运行–死亡
E:新建–就绪–运行–等待阻塞–同步阻塞–就绪–运行–死亡

线程组

Java中使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,Java允许程序直接对线程组进行控制。

//线程默认情况下属于main线程组
Thread.currentThread().getThreadGroup().getName();

修改线程组:创建一个线程组,创建其他线程的时候,把其他线程的组指定为我们自己新建线程组

ThreadGroup tq = new ThreadGroup("这是一个新的组");
MyRunnable my = new MyRunable();
Thread t1 = new Thread(tg,my,"james");
tg.setDaemon(true);//表示该组线程都是守护线程,好处可以对一个组的线程进行统一控制操作,但较少使用

线程池

程序启动一个新线程成本是比较高的,因为它涉及到要与操作系统进行交互。而使用线程池可以很好的提高性能,尤其是当程序中要使用大量生命周期很短的线程时,应该考虑使用线程池。
- 线程池中的每个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用。
- 在JDK5之前,我们必须手动实现自己的线程池,从JDK5开始,java内置支持线程池。(Executors工厂类)

如何实现线程池代码?
A:创建一个线程池对象,控制要创建几个线程对象

 public static ExecutorService newFixedThreadPool(int nThreads);

B:这种线程池的线程可以执行:
可以执行Runnable对象或者Callable对象代表的线程,做一个类可以实现Runnable接口
C:调用如下方法即可

Future<?> submit(Runnable task)
<T> Future<T> submit(Callable<T> task)

D:结束线程池
代码如下:

public class MyRunnable implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}
///////////////////////////////////////
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExecutorsDemo {
    public static void main(String[] args) {
        // 创建一个线程池对象,控制要创建几个线程对象。
        ExecutorService pool = Executors.newFixedThreadPool(2);
        // 可以执行Runnable对象或者Callable对象代表的线程
        pool.submit(new MyRunnable());
        pool.submit(new MyRunnable());
        // 结束线程池
        pool.shutdown();
    }
}

定时器

定时器是一个应用十分广泛的线程工具,可用于调度多个定时任务以后台线程的方式执行。java中,可以用过Timer和TimerTask类来实现定义调度的功能。开发中使用Quartz,一个java编写的开源调度框架。

import java.util.Timer;
import java.util.TimerTask;

public class TimerDemo {
    public static void main(String[] args) {
        // 创建定时器对象
        Timer t = new Timer();
        // 3秒后执行爆炸任务
        t.schedule(new MyTask(t), 3000);
    }
}

// 做一个任务
class MyTask extends TimerTask {
    private Timer t;

    public MyTask() {
    }

    public MyTask(Timer t) {
        super();
        this.t = t;
    }

    @Override
    public void run() {
        System.out.println("boom");
        t.cancel();
    }
}

思考题

1.多线程有几种实现方案,分别是哪几种?
两种。
继承Thread类
实现Runnable接口
扩展一种:实现Callable接口。这个和线程池结合。

2.同步有几种方式,分别是什么?
两种。同步代码块和同步方法。

3.启动一个线程使用run()还是start(),有什么区别
start()
run():封装了被线程执行的代码,直接调用仅仅是普通方法的调用。
start():启动线程,并由JVM自动调用run()方法。

4.sleep()和wait()方法的区别
sleep:必须指定时间;不释放锁
wait:可以指定也可以不指定时间;释放锁

5.为什么wait(),notify(),notifyAll()等方法都定义在Object类中
因为这些方法的调用时依赖于锁对象的,而同步代码块的锁对象是任意锁。而Object代表任意的对象,所以定义在这里。
6.线程的生命周期图

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值