目录
进程:进程是程序的一次动态执行过程,它经历了从代码加载、执行到执行完毕的整个过程。是操作系统分配资源的基本单位。
线程:是进程的一个执行单元。也被称为轻量级进程。是处理器任务调度和处理的基本单位。
一个进程中可以有多个线程,多个线程共享进程的堆和方法区 (JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器、虚拟机栈 和 本地方法栈。
程序计数器为什么是私有的?
程序计数器主要有下面两个作用:
所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。
虚拟机栈和本地方法栈为什么是私有的?
所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。
一句话简单了解堆和方法区
堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
多进程和多线程区别
多进程:操作系统中同时运行的多个程序
多线程:在同一个进程中同时运行的多个任务
- // Thread类定义:
- public class Thread extends Object implements Runnable {}
可以看出Thread也是实现了Rubnable接口的。
- public class ThreadDemo extends Thread{
- private String name;
- public ThreadDemo(String name) {
- this.name = name;
- }
-
- @Override
- public void run() {
- for (int i = 0; i < 1000; i++) {
- System.out.println(this.name+">>>>>>>>>>>"+i);
- }
- }
- }
-
-
-
- public class Main {
- public static void main(String[] args) {
-
- ThreadDemo t1 = new ThreadDemo("T1");
- ThreadDemo t2 = new ThreadDemo("T2");
- ThreadDemo t3 = new ThreadDemo("T3");
-
- t1.start();
- t2.start();
- t3.start();
-
- // t1.run();
- // t2.run();
- // t3.run();
- }
-
- }
继承Thread,重写run方法,运行start方法,会默认调用run方法。
Thread虽然可以实现多线程,确定显而易见:面向对象的单继承局限性。接口就不一样了,可以多继承。
- @FunctionalInterface
- public interface Runnable{
- public void run();
- }
都需要重写run方法。不过运行方法不一样。
- RunnableDemo t1 = new RunnableDemo("T1");
- RunnableDemo t2 = new RunnableDemo("T2");
- RunnableDemo t3 = new RunnableDemo("T3");
-
- new Thread(t1).start();
- new Thread(t2).start();
- new Thread(t3).start();
利用Runnable方案实现更加能体现面向对象思维,有点类似于代理设计模式。
使用Runnable接口实现多线程可以解决单继承局限性,但没有的返回值。Callable接口可以有返回值。
- @FunctionalIterface
- public interface Callable
{ - public T call() throws Exception;
- }
- public class CallableDemo implements Callable
{ - private String name;
- public CallableDemo(String name) {
- this.name = name;
- }
-
- @Override
- public Integer call() throws Exception {
- int num = 0;
- for (int i = 0; i < 10; i++) {
- System.out.println(this.name+">>>>>>>>>>"+i);
- num++;
- }
- return num;
- }
- }
-
-
- public class Demo3 {
- public static void main(String[] args) throws Exception {
- CallableDemo t1 = new CallableDemo("t1");
- CallableDemo t2 = new CallableDemo("t2");
- CallableDemo t3 = new CallableDemo("t3");
-
- FutureTask
ft1 = new FutureTask<>(t1); - FutureTask
ft2 = new FutureTask<>(t2); - FutureTask
ft3 = new FutureTask<>(t3); -
- Thread thread1 = new Thread(ft1);
- thread1.start();
- new Thread(ft2).start();
- new Thread(ft3).start();
-
- System.out.println(thread1.getName()+"==================="+ft1.get());
- System.out.println(ft2.get());
- System.out.println(ft3.get());
- }
- }
FutureTask类常用方法:
Thread类的一个构造方法:
Callable接口实现采用泛型技术实现,继承需要重写call方法,再通过FutureTask包装器包装,传入后实例化Thread类实现多线程。
其中FutureTask类是Runnable接口的子类,所以才可以利用Thread类的start方法启动多线程,读者可以将call方法假设为有返回值的run方法。
public static void yield() 线程让步
setPriority()设置线程优先级,10最高,1最低,默认5
线程安全问题指的是,多个线程同时操作同一个共享资源的时候,可能会出现业务安全问题。
- public class Account {
-
- private String id; //卡号
- private Double money; //余额
-
- public Account() {
- }
-
- public Account(String id, Double money) {
- this.id = id;
- this.money = money;
- }
-
- public void drawMoney(double money) {
- //先搞清楚谁来取钱
- String name = Thread.currentThread().getName();
- if(this.money>=money) {
- System.out.println(name+"来取钱,成功取了:"+money);
- this.money-=money;
- } else {
- System.out.println(name+"来取钱,钱不够了!");
- }
- System.out.println("取钱后余额:"+this.money);
- }
- }
-
-
- public class DrawThread extends Thread{
-
- private Account account;
-
- public DrawThread(Account account,String name) {
- super(name);
- this.account = account;
- }
-
- @Override
- public void run() {
- account.drawMoney(10000);
- }
- }
-
-
-
- public class Demo4 {
-
- public static void main(String[] args) {
-
- Account acc = new Account("ID", 10000.0);
-
- DrawThread dt1 = new DrawThread(acc, "小红");
- DrawThread dt2 = new DrawThread(acc, "小明");
-
- dt1.start();
- dt2.start();
-
- }
- }
运行结果:
为解决上述问题,可以使用同步思想。最常见的同步思想就是加锁,意思是每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动释放锁,然后其他线程才能再加锁进来。
Java提供了三种加锁方式
作用就是把访问共享数据的代码锁起来,以此保证线程安全。
- //锁对象:必须是一个唯一的对象(同一个地址)
- synchronized(锁对象){
- //...访问共享数据的代码...
- }
只需修改DrawThread类中的代码即可。
- public void drawMoney(double money) {
- //先搞清楚谁来取钱
- String name = Thread.currentThread().getName();
- synchronized (this) {
- if(this.money>=money) {
- System.out.println(name+"来取钱,成功取了:"+money);
- this.money-=money;
- } else {
- System.out.println(name+"来取钱,钱不够了!");
- }
- System.out.println("取钱后余额:"+this.money);
- }
-
- }
锁对象如何选择?
其实同步方法,就是把整个方法给锁住,一个线程调用这个方法,另一个线程调用的时候就执行不了,只有等上一个线程调用结束,下一个线程调用才能继续执行。
- // 同步方法
- public synchronized void drawMoney(double money) {
- // 先搞清楚是谁来取钱?
- String name = Thread.currentThread().getName();
- // 1、判断余额是否足够
- if(this.money >= money){
- System.out.println(name + "来取钱" + money + "成功!");
- this.money -= money;
- System.out.println(name + "来取钱后,余额剩余:" + this.money);
- }else {
- System.out.println(name + "来取钱:余额不足~");
- }
- }
同步方法也是有锁对象,只不过这个锁对象没有显示的写出来而已。
Lock锁是JDK5版本专门提供的一种锁对象,通过这个锁对象的方法来达到加锁,和释放锁的目的,使用起来更加灵活。
- 1.首先在成员变量位子,需要创建一个Lock接口的实现类对象(这个对象就是锁对象)
- private final Lock lk = new ReentrantLock();
- 2.在需要上锁的地方加入下面的代码
- lk.lock(); // 加锁
- //...中间是被锁住的代码...
- lk.unlock(); // 解锁
- // 创建了一个锁对象
- private final Lock lk = new ReentrantLock();
-
- public void drawMoney(double money) {
- // 先搞清楚是谁来取钱?
- String name = Thread.currentThread().getName();
- try {
- lk.lock(); // 加锁
- // 1、判断余额是否足够
- if(this.money >= money){
- System.out.println(name + "来取钱" + money + "成功!");
- this.money -= money;
- System.out.println(name + "来取钱后,余额剩余:" + this.money);
- }else {
- System.out.println(name + "来取钱:余额不足~");
- }
- } catch (Exception e) {
- e.printStackTrace();
- } finally {
- lk.unlock(); // 解锁
- }
- }
- }
经典问题就是生产者消费者问题。
线程池就是一个可以复用线程的技术。
假设:用户每次发起一个请求给后台,后台就创建一个新的线程来处理,下次新的任务过来肯定也会创建新的线程,如果用户量非常大,创建的线程也讲越来越多。然而,创建线程是开销很大的,并且请求过多时,会严重影响系统性能。
在JDK5版本中提供了代表线程池的接口ExecutorService,而这个接口下有一个实现类叫ThreadPoolExecutor类,使用ThreadPoolExecutor类就可以用来创建线程池对象。
- ExecutorService pool = new ThreadPoolExecutor(
- 3, //核心线程数有3个
- 5, //最大线程数有5个。 临时线程数=最大线程数-核心线程数=5-3=2
- 8, //临时线程存活的时间8秒。 意思是临时线程8秒没有任务执行,就会被销毁掉。
- TimeUnit.SECONDS,//时间单位(秒)
- new ArrayBlockingQueue<>(4), //任务阻塞队列,没有来得及执行的任务在,任务队列中等待
- Executors.defaultThreadFactory(), //用于创建线程的工厂对象
- new ThreadPoolExecutor.CallerRunsPolicy() //拒绝策略
- );
临时线程什么时候创建?
新任务提交时,发现核心线程都在忙、任务队列满了、并且还可以创建临时线程,此时会创建临时线程。
什么时候开始拒绝新的任务?
核心线程和临时线程都在忙、任务队列也满了、新任务过来时才会开始拒绝任务。
创建好线程池之后,接下来我们就可以使用线程池执行任务了。线程池执行的任务可以有两种,一种是Runnable任务;一种是callable任务。下面的execute方法可以用来执行Runnable任务。
- public class MyRunnable implements Runnable{
- @Override
- public void run() {
- System.out.println(Thread.currentThread().getName()+"===========> 666");
-
- try {
- Thread.sleep(5000);
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
-
-
-
- public class Demo5 {
-
- public static void main(String[] args) {
-
- ExecutorService pool = new ThreadPoolExecutor(
- 3, //核心线程数有3个
- 5, //最大线程数有5个。 临时线程数=最大线程数-核心线程数=5-3=2
- 8, //临时线程存活的时间8秒。 意思是临时线程8秒没有任务执行,就会被销毁掉。
- TimeUnit.SECONDS,//时间单位(秒)
- new ArrayBlockingQueue<>(4), //任务阻塞队列,没有来得及执行的任务在,任务队列中等待
- Executors.defaultThreadFactory(), //用于创建线程的工厂对象
- new ThreadPoolExecutor.CallerRunsPolicy() //拒绝策略
- );
-
- MyRunnable target = new MyRunnable();
-
- pool.execute(target); // 线程池会自动创建一个新线程,自动处理这个任务,自动执行的!
- pool.execute(target); // 线程池会自动创建一个新线程,自动处理这个任务,自动执行的!
- pool.execute(target); // 线程池会自动创建一个新线程,自动处理这个任务,自动执行的!
- //下面4个任务在任务队列里排队
- pool.execute(target);
- pool.execute(target);
- pool.execute(target);
- pool.execute(target);
-
- //下面2个任务,会被临时线程的创建时机了
- pool.execute(target);
- pool.execute(target);
- // 到了新任务的拒绝时机了!
- pool.execute(target);
- }
- }
执行Callable任务需要用到下面的submit方法
- public class MyCallable implements Callable
{ - private int n;
- public MyCallable(int n) {
- this.n = n;
- }
-
- // 2、重写call方法
- @Override
- public String call() throws Exception {
- // 描述线程的任务,返回线程执行返回后的结果。
- // 需求:求1-n的和返回。
- int sum = 0;
- for (int i = 1; i <= n; i++) {
- sum += i;
- }
- return Thread.currentThread().getName() + "求出了1-" + n + "的和是:" + sum;
- }
- }
-
-
-
- public class ThreadPoolTest2 {
- public static void main(String[] args) throws Exception {
- // 1、通过ThreadPoolExecutor创建一个线程池对象。
- ExecutorService pool = new ThreadPoolExecutor(
- 3,
- 5,
- 8,
- TimeUnit.SECONDS,
- new ArrayBlockingQueue<>(4),
- Executors.defaultThreadFactory(),
- new ThreadPoolExecutor.CallerRunsPolicy());
-
- // 2、使用线程处理Callable任务。
- Future
f1 = pool.submit(new MyCallable(100)); - Future
f2 = pool.submit(new MyCallable(200)); - Future
f3 = pool.submit(new MyCallable(300)); - Future
f4 = pool.submit(new MyCallable(400)); -
- // 3、执行完Callable任务后,需要获取返回结果。
- System.out.println(f1.get());
- System.out.println(f2.get());
- System.out.println(f3.get());
- System.out.println(f4.get());
- }
- }
方便但是不推荐使用,这是《阿里巴巴Java开发手册》提供的强制规范要求。
并发指一个CPU同时处理多个线程,为了所以线程都能执行到,CPU采用轮询机制为每个线程服务,由于CPU切换速度快且执行快,给我们的感觉就像这些进程在同时执行,这就是并发。
这就好像两个人用同一把铁锨,轮流挖坑,一小时后,两个人各挖一个小一点的坑,要想挖两个大一点得坑,一定会用两个小时。
并行是指同一时刻,多个线程在多个CPU上同时执行。
就好像两个人各拿一把铁锨在挖坑,一小时后,每人一个大坑。所以无论从微观还是从宏观来看,二者都是一起执行的。
最后一个问题,多线程到底是并发还是并行呢?
其实多个线程在我们的电脑上执行,并发和并行是同时存在的。