JAVA线程

一、线程相关概念

(一)程序、进程和线程的区别

  • 程序

程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。

  • 进程

进程是程序的一次执行过程,是系统运行的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。

  • 线程

线程,与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比进程小得多,线程也被称为轻量级进程。

(二)并行和并发

  • 并行:指两个或多个事件在同一时刻发生(同时发生 同时处理 执行者不是一个)。
  • 并发:指两个或多个事件在同一个时间段内发生【交替的发生 执行者是一个】。

1、电脑(单核)在执行任务时是采用并发还是并行?

使用的是并发,因为电脑上只有一个CPU,但是电脑上却可以同时运行多个程序,这是一种假象,因为CPU的计算速度极快,10^-9每秒就会计算一次,而人能感觉到的时间流失是秒,所以电脑给我们一种感觉好像电脑上程序在同时执行。

二、Java实现多线程的三种方式

(一)继承Thread

Thread就是一个线程类,里面定义所有和线程相关的功能,只有是这个类才能开启的任务,才能被CPU单独去执行。

Java中线程执行的时候是采用的并发。学习的过程中不要纠结于多个线程执行的结果,因为是随机在执行,抢占式的在使用CPU资源。

1、方法介绍

说明方法名
在线程开启后,此方法将被调用执行void run()
在线程开始执行,java虚拟机会调用run()方法void start()

2、实现步骤

  • 定义一个类MyThread继承Thread类
    • 在MyThread类中重写run()方法,添加自己的任务
    • 创建MyThread类的对象
    • 启动线程

3、代码示例

package com.why.demo02;

// 继承Thread类,MyThread就具备所有和线程相关的功能
public class MyThread extends Thread{
// 重写run方法, 条件自己的任务
@Override
public void run() {
    // 添加一个任务打印一百次:MyThread线程任务 ----
    for (int i = 1; i < 101; i++) {
        System.out.println("MyThread线程任务 ---- " + i);
    }
 }
}
package com.ujiuye.demo02;

public class UseMyThread {
public static void main(String[] args) {
    // System.out.println( 1 / 0);
    // 创建一个MyThread对象
    MyThread mt = new MyThread();
    // 调用start方法开启线程
    mt.start();
    // mt.run(); 千万不要调用run方法,因为并没有开启线程,只是一个对象调用一次方法而已.
    // 这个循环属于main方法所在的线程,就是换一个叫做"main"线程,这线程就是主线程
    for (int i = 0; i < 100; i++) {
        System.out.println("主线程中的任务---" + i);
    }
 }
}

继承开启多线程的简化版:匿名内部类的方式

Thread 变量名 = new Thread() {
        重写run方法;
}
变量名.start();
new Thread() {
        重写run方法;
}.start();
public class UseNoNameClassImpThread {
    public static void main(String[] args) {
        // 匿名内部方式继承Thread类开启多线程
        Thread t = new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println("线程1-----"+i);
                }
            }
        };
        t.start();
        new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println("线程2-----"+i);
                }
            }
        }.start();
    }
}
继承Thread实现多线程不好的地方,线程和任务内容耦合性太强,这个线程只能执行run方法中编写任务内容。无法执行别的任务,,且这个任务也只能被这个线程执行。

(二)实现Runnable接口

Runnable是一个接口。仅仅用来描述线程要执行的任务。它的实现类就是线程执行的任务,实现类不是线程对象。

1、方法介绍

方法名说明
Thread(Runnable target)分配一个新的Thread对象
Thread(Runnable target,String name)分配一个新的Thread对象

解释:

Thread的构造方法创建对象的时候传入了Runnable接口的对象 ,Runnable接口对象重写run方法相当于指定线程任务,创建线程的时候绑定了该线程对象要干的任务。

Runnable的对象称之为:线程任务对象 不是线程对象 必须要交给Thread线程对象。
通过Thread的构造方法, 就可以把任务对象Runnable,绑定到Thread对象中, 将来执行start方法,就会自动执行Runable实现类对象中的run里面的内容。

2、实现步骤

  • 定义一个类MyRunnable实现Runnable接口
  • 在MyRunnable类中重写run()方法
  • 创建MyRunnable类的对象
  • 创建Thread类的对象,把MyRunnable对象作为构造方法的参数
  • 启动线程

3、代码示例

package com.offcn.demo2;
// 1.定义一个MyRunnable类实现Runnable接口
public class MyRunnable implements Runnable{
    // 2.重写接口中的run方法.定义线程任务内容
    @Override
    public void run() {
        for (int i = 1; i < 101; i++) {
            System.out.println("任务对象----"+i);
        }
    }
}
public class UseMyRunnable {
    public static void main(String[] args) {
        // 3.创建任务类对象
        MyRunnable m1 = new MyRunnable();
        // 4.创建线程对象,把任务对象绑定到线程中
        Thread t1 = new Thread(m1);
        t1.start();

        for (int i = 0; i < 100; i++) {
            System.out.println("主线程------"+i);
        }
    }
}

匿名内部类实现Runnable接口,开启多线程

Runnable r = new Runnable() {
        重写run方法
};
Thread t = new Thread(r);
Thread t2 = new Thread(new Runnable() {
        重写run方法
});
public class NoNameRunnableObject {
    public static void main(String[] args) {
        // 匿名内部类为Runnable提供实现类
        Runnable r = new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i < 20; i++) {
                    System.out.println("任务1-----"+i);
                }
            }
        };
        // 任务对象绑定到线程对象中,相同的任务可以被多个线程对象执行.
        // 解开了之前继承实现多线程下任务和线程之间的耦合性.
        Thread t1 = new Thread(r );
        Thread t2 = new Thread(r);
        t1.start();
        t2.start();
        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i < 30; i++) {
                        System.out.println("任务2------我爱学习java");
                }
            }
        });
        t3.start();
    }
}

(三)实现Callable接口

这种实现多线程方式类型有三个:Callable接口,FutureTask类,Thread类。

1、方法介绍

方法名说明
V call()
计算结果,如果无法计算结果,则抛出一个异常,线程要执行任务就写在call方法中。此方法的返回值就是线程任务执行结束后给调用者的反馈。
FutureTask(Callable callable)
创建一个 FutureTask对象,一旦运行就执行给定的 Callable接口你重写的call的内容
V get()
如有必要,等待计算完成,然后获取其结果,获取的就是call方法的返回值,这个get方法属于延迟方法,因为调用完之后不会立即执行,而是等到Callable接口实现call执行完毕之后才会执行了。获取就是call方法的返回值

FutureTask实现了RunnableFuture接口,而RunnableFuture是Runnable接口子接口,所以FutureTask对象就可以看做一个Runnable接口实现类对象使用,可以通过Thread类的Thread(Runnable target)构造方法,把Ruture Task对象绑定到线程对象中。

2、实现步骤

定义一个类MyCallable实现Callable接口

  • 在MyCallable类中重写call()方法
  • 创建MyCallable类的对象
  • 创建Future的实现类FutureTask对象,把MyCallable对象作为构造方法的参数
  • 创建Thread类的对象,把FutureTask对象作为构造方法的参数
  • 启动线程
    • 再调用get方法,就可以获取线程结束之后的结果

3、代码示例

Callable实现类
//实现Callable接口
public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        // 线程任务
        for (int i = 1; i < 100; i++) {
            System.out.println("女神 咱俩交往吧, I love you!!!");
        }
        // 女生给你的反馈
        return "一起去如家";
    }
}
public class UseMyCallable {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 3.MyCallable对象
        MyCallable mc = new MyCallable();
        // 4.创建一个FutureTask对象, 把MyCallable绑定到未来任务对象中
        FutureTask<String> task = new FutureTask<>(mc);

        // 5.把未来任务对象绑定到Thread类中
        Thread t = new Thread(task);
        t.start();
        for (int i = 0; i < 100; i++) {
            System.out.println("主线程-女生不要答应");
        }
        // 一定要在线程开启之后,在调用get方法,否则线程就没有开启的机会.
        String result = task.get();
        System.out.println("女神的反馈:"+result);
    }
}

 

(四)三种创建多线程的使用场景

1、继承的方式:适合于这个任务只想被一个线程的对象执行的情况

2、实现Runnable接口方式:适合于一个任务想被多个线程执行的情况

3、实现Callable接口方式:也适合一个任务想被多个线程执行的情况,你还想得倒任务的执行结果

三、线程Thread类常用方法

(一)线程API之线程名称

1、方法介绍

方法名说明
void setName(String name)将此线程的名称更改为等于参数name,除了可以使用setName设置线程名称,也可以是Thread类提供构造方法执行线程名称.Thread(String name), Thread(Runnable r, String name)
String getName()返回此线程的名称
static Thread currentThread()返回对当前正在执行的线程对象的引用

2、代码示例

public class UseThreadName {
    public static void main(String[] args) {
        // 线程如果没有指定名称,也有默认名称: Thread-数字 数字从0开始
        Thread t1 = new Thread();
        System.out.println(t1.getName());// Thread-0
        Thread t2 = new Thread();
        t2.setName("线程2"); // 修改线程名称
        System.out.println(t2.getName());
        // 通过构造方法执行线程名称: Thread(Stirng name)
        Thread t3 = new Thread("线程1");
        System.out.println(t3.getName());
        // 通过构造方法执行线程名称: Thread(Runnable r, String name)
        Thread t4 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("任务");
            }
        }, "线程3");
        System.out.println(t4.getName());

        // 获取main方法所在线程对象
        Thread t = Thread.currentThread();
        System.out.println(t.getName());
        // Exception in thread "main" 在一个叫main线程中出现了异常
        // System.out.println(1 / 0 );
    }
}

(二)线程API之线程休眠

1、方法介绍

方法名说明
static void sleep(long millis)是一个静态方法,而且方法上还有编译时异常的声明,使用时一定要处理异常。作用使当前正在执行的线程停留(暂停执行)指定的毫秒数,1s = 1000ms,过了指定的时间还会醒,可以继续执行任务。

2、代码示例

public class UseThreadSleepMethod {
    public static void main(String[] args) {
        // 匿名内部类体现Thread子类
        Thread t1 = new Thread("线程1"){
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        Thread.sleep(1000); // 只能try..catch处理异常,因为Thread中
run没有异常的声明,子类重写也不能声明异常
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(getName()+"----"+i);
                }
            }
        };
        t1.start();
        // 匿名内部类提供Runnable的实现类
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "----"+i);
                }
            }
        }, "线程2");
        t2.start();
    }
}

(三)线程API之线程优先级

1、方法介绍

方法名说明
final int getPriority()返回此线程的优先级
final void setPriority(int newPriority)更改此线程的优先级是5;线程优先级的范围是:1-10,数字越大优先级越高。以后设置优先级也不会直接写数字,而是使用Thread类中提供三个常量值设置优先级属性。

  •  设置线程的优先级【线程的执行java遵循抢占式CPU资源机制,不随着人的意志为转移,规律认为决定不了】,表现就是在一段时间内容,前半段时间内高优先级执行次数多,后半段时间低优先级的执行次数多。
  • CPU会尽量的在前期执行优先级高的【不是百分百】

2、代码示例

public class UseThreadPriority {
    public static void main(String[] args) {

        Thread t1 = new Thread("线程1") {
            @Override
            public void run() {
                for (int i = 1; i <= 50; i++) {
                    System.out.println(getName() + "----" + i);
                }
            }
        };
        Thread t2 = new Thread("线程2") {
            @Override
            public void run() {
                for (int i = 1; i <= 50; i++) {
                    System.out.println(getName() + "----" + i);
                }
            }
        };
        // 获取线程对象默认优先级
        System.out.println(t1.getPriority()); // 5
        System.out.println(t2.getPriority()); // 5
        /* // 练习:获取主线程优先级
        Thread t = Thread.currentThread();
        System.out.println(t.getName() + "----" + t.getPriority()); // main----5*/
        // 设置线程优先级 数字必须是[1, 10] 超出范围就会报错
        t1.setPriority(Thread.MAX_PRIORITY);
        t2.setPriority(Thread.MIN_PRIORITY);

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

 

(四)线程API之线程礼让

1、方法介绍

方法名说明
static void yield()
线程让步,暂停当前正在执行的线程任务,把执行机会让给优先级相同或更高的线程,若队列中没有同优先级的线程,忽略此方法; 当前线程让出CPU资源后,依然有获取CPU的机会,并不是让步之后就不能获取到CPU资源了。

2、代码示例

public class YieldMethod {
public static void main(String[] args) {
    Thread t1 = new Thread("任务1") {
        @Override
        public void run() {
            for (int i = 0; i < 50; i++) {
                // 任务1碰到偶数就礼让
                if(i % 2 == 0) {
                    Thread.yield();
                }
                System.out.println(getName()+"------" + i);
            }
        }
    };
    Thread t2 = new Thread("任务2") {
        @Override
        public void run() {
            for (int i = 0; i < 50; i++) {
                System.out.println(getName()+"------" + i);
            }
        }
    };
    // t1和t2的优先级一样,都是5,能触发yield方法.
    t1.start();
    t2.start();
    }
}

基本都是t2先执行完,t1才会执行完。因为t1经常让步。

(五)线程API之线程中断

线程中断方法不是把任务执行中断掉,而是终端的线程状态。

1、方法介绍

方法名说明
public void interrupt()
中断这个线程状态,不是中断任务。

2、代码示例

public class UseThreadInterrupt {
    public static void main(String[] args) {
        Thread t1 = new Thread("why"){
            @Override
            public void run() {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                for (int i = 1; i <= 5; i++) {
                    System.out.println(getName()+"i");
                }
            }
        };
        t1.start();
        t1.interrupt(); // 看到程序执行的之后,就不回你再停留5秒才走循环体. 线程状态被中断会异常,但是咱们使用try...catch处理异常,程序出了异常还能继续执行.
    }
}

(六)线程API之后台线程

1、方法介绍

方法名说明
public final void
setDaemon(boolean
on)
将此线程标记为daemon线程或用户线程。当运行的唯一线程都是守护进程线程时,Java虚拟机将退出。
boolean isDaemon()
判断一个线程是否为守护线程,是守护线程返回true,否则返回false.

2、代码示例

public class UseThreadDaemon {
    public static void main(String[] args) {

        Thread t = Thread.currentThread();
        boolean daemon = t.isDaemon();
        System.out.println(daemon ? "主线是守护线程" : "主线程不是守护线程");

        // 自己创建的线程对象,默认都不是守护线程.
        Thread t1 = new Thread("大王"){
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println("我是大王我怕谁???");
                }
            }
        };
        System.out.println(t1.isDaemon());
        Thread t2 = new Thread("奴隶"){
            @Override
            public void run() {
                while (true){
                    System.out.println("我是守护者,要为大王服务....");
                }
            }
        };
        t2.setDaemon(true); // t2就变为守护线程了,就具备守护线程特点, 非守护线程任务结束了,
        // 那么这个守护线程过一会也会挂掉.
        System.out.println(t2.isDaemon());
        
        t1.start();
        t2.start();
    }
}

四、线程安全问题

(一)问题描述

1、买票案例

案例需求:
        某电影院目前正在上映国产大片:一路向西,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票
实现步骤:
        - 定义一个类SellTicket实现Runnable接口,里面定义一个成员变量:private int tickets = 100;
        - 在SellTicket类中重写run()方法实现卖票,代码步骤如下
        - 判断票数大于0,就卖票,并告知是哪个窗口卖的
        - 卖了票之后,总票数要减1
        - 票卖没了,线程停止
        - 定义一个测试类SellTicketDemo,里面有main方法,代码步骤如下
        - 创建SellTicket类的对象
        - 创建三个Thread类的对象,把SellTicket对象作为构造方法的参数,并给出对应的窗口名称
        - 启动线程

2、代码示例

public class SellTicket implements Runnable {
    // 定义变量模拟要卖的100张票
    private int ticket = 100;

    @Override
    public void run() {
        while (ticket > 0) {
            // 卖出一张票数-1
            ticket--;
            System.out.println(Thread.currentThread().getName()+"---卖出的是第"+(ticket+1)+"张, 剩余"+ticket+"张");
        }
    }
}



// 线程安全问题演示
public class ThreadSafeProblem {
    public static void main(String[] args) {
        // 卖票任务
        SellTicket st = new SellTicket();
        // 创建3个线程对象,模拟三个窗口
        Thread t1 = new Thread(st, "窗口1");
        Thread t2 = new Thread(st, "窗口2");
        Thread t3 = new Thread(st, "窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}

3、出现问题

 买票出现的问题:

  1. 相同的票出现了多次
  2. 出现了负数的票

问题产生的原因:线程执行的随机性导致的,可能再卖票过程中丢失cpu的执行权,导致出现问题。

(二)解决方案

原子操作

原子操作就是不可分割的操作,例如一个售票窗口在售票的过程中的代码就是一个不可分割的操作。就是一条线程在操作某个任务的时候,一定要保证他的操作必须完全都结束,其他的线程才有机会做这个操作。体现:让抢CPU变得同步【有序】

处理方案:

  1. 同步代码块
  2. 同步方法
  3. Lock接口(锁对象)

1、同步代码块

(1)格式
synchronized(锁对象) {
        原子操作的代码;【有可能出现线程安全问题的代码】
}
(2)说明
  1.  synchronized 同步的意思
  2. 锁对象可以是任何引用类型的java对象,只要是一个java对象就可以当做锁对象
  3. 锁对象一旦被确定下来要保证唯一性, 锁对象只能有一份
(3)同步代码块执行原理

一段代码一旦使用同步代码块之后,线程只要进入同步代码块,就要先获取锁对象,只有拿到锁对象才能进入同步代码块执行任务,在这个线程执行任务期间,CPU可能会把资源切换到其他线程,即使其他线程也要进入这个同步代码块,因为锁对象已经被之前的线程拿走了,它没有锁对象,依然进不来,只能等之前线程执行完,把锁对象归还,其他线程才有机会拿到锁对象,执行里面任务。

(4)优缺点

优点:解决了多线程的数据安全问题

缺点:当线程很多时,因为每个线程都会去判断同步上的锁。这是很耗费资源的,无形中会降低程序的运行效率。

(5)代码示例
public class SellTicket implements Runnable {
    // 定义变量模拟要卖的100张票
    private int ticket = 100;
    private StringBuilder sb = new StringBuilder();
    @Override
    public void run() {
        while (ticket > 0) { // "a"首先是String对象. 且常量字符串只会在方法区常量池中存储一份.
        // synchronized (new String("a")) { // 这种锁是锁不住的,每次获取锁对象都new一个新的对象.无法同步代码中原子性操作.
        // synchronized (this) { // 外面只创建一个SellTicket对象, 且三个线程绑定的都是这个SellTicket对象. 因此this就只代表外界创建这个st对象. 此时依然能保证锁对象的唯一.
        // synchronized (sb) { sb是SellTicket的一个对象属性,SellTicket在外界只创建了一个st对象,这个st对象sb属性也就只有一个.所以能保证锁对象的唯一.
            synchronized (SellTicket.class) { // 类名.class就是获取这个类在方法区中加载字节文件之后形成字节码文件对象.一个类字节文件只会在方法区中加载一次.这个类字节文件对象只有一个.它也能充当锁对象
            if (ticket > 0 ){ // 防止当ticket等于1的时候, 三条线程执行时都满足循环判断
                // ticket>0成立,进入循环体. 执行同步代码快操作时出现卖负票的情况
                // 卖出一张票数-1
                ticket--;
                System.out.println(Thread.currentThread().getName() + "---卖出的是第" +(ticket + 1) + "张, 剩余" + ticket + "张");
            }
        }
    }
 }
}



// 线程安全问题演示
public class ThreadSafeProblem {
    public static void main(String[] args) {
        // 卖票任务
        SellTicket st = new SellTicket();
        // 创建3个线程对象,模拟三个窗口
        Thread t1 = new Thread(st, "窗口1");
        Thread t2 = new Thread(st, "窗口2");
        Thread t3 = new Thread(st, "窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}

2、同步方法

(1)同步成员方法
格式:
修饰符 synchronized 返回值类型 方法名 ( 方法参数 ) {
        方法体;
}
什么时候使用同步成员方法?
如果你发现你写的一个成员方法 , 这个方法内部所有的代码都使用同步代码块给包裹了 , 并且使用的锁对象是this的时候 . 就可以使用同步成员方法简化这个方法。
修饰符 返回值类型 方法名 ( 方法参数 ) {
        synchronized ( this ) {
                方法体;
        }
}

因为同步成员方法默认的锁对象就是:this

案例代码:
public class SellTicket implements Runnable {
    // 定义变量模拟要卖的100张票
    private int ticket = 100;
    @Override
    public void run() {
        while (ticket > 0) {
        sell();
    }
}

// 把卖票的过程抽取出来定义到一个成员方法中
// 咱们写的一个成员方法sell,这个方法内部所有的代码都使用同步代码块给包裹了,并且使用的锁对象是this的时候
// 这种写法可以使用同步成员方法简化
/* public void sell() {
synchronized (this) { // this-->st
if (ticket > 0 ){
ticket--;
System.out.println(Thread.currentThread().getName() + "---卖出的是第" +
(ticket + 1) + "张, 剩余" + ticket + "张");
}
}
}*/


// 这就是同步方法. 同步成员方法的锁对象就是this. 简化的就是上面sell中的代码
public synchronized void sell() {
    if (ticket > 0 ){
        ticket--;
        System.out.println(Thread.currentThread().getName() + "---卖出的是第" + (ticket + 1) + "张, 剩余" + ticket + "张");
    }
  }
}


public class ThreadSafeProblem {
    public static void main(String[] args) {
        // 卖票任务
        SellTicket st = new SellTicket();
        // 创建3个线程对象,模拟三个窗口
        Thread t1 = new Thread(st, "窗口1");
        Thread t2 = new Thread(st, "窗口2");
        Thread t3 = new Thread(st, "窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}
(2)同步静态方法
格式:
修饰符 static synchronized 返回值类型 方法名 ( 方法参数 ) {
        方法体;
}
什么时候使用同步静态方法?
如果你发现你写的一个静态方法 这个方法内部所有的代码都使用同步代码块给包裹了 并且使用的锁对象是当前类类对象的时候 ( 类名 . class )。 就可以使用同步静态方法简化这个方法。
修饰符 static 返回值类型 方法名 ( 方法参数 ) {
        synchronized ( 类名 . class ) {
                方法体;
        }
}

因为同步静态方法默认值的锁对象就是:当前类的类对象,类名.class

案例代码:
public class SellTicket implements Runnable {
    // 定义变量模拟要卖的100张票
    private static int ticket = 100;

    @Override
    public void run() {
        while (ticket > 0) {
        sell();
    }
}


// 定义一个静态方法sell,里面所有的代码都使用同步代码块包裹.并且使用的锁对象是当前类字节码文件对象(当前类的类对象)
/* public static void sell() {
synchronized (SellTicket.class) {
if (ticket > 0 ){
ticket--;
System.out.println(Thread.currentThread().getName() + "---卖出的是第" +
(ticket + 1) + "张, 剩余" + ticket + "张");
}
}
}*/


    // 同步静态方法. 默认使用锁对象就是当前类的类对象(SellTicket.class),简化的就是上面sell方法的写法
    public static synchronized void sell() {
        if (ticket > 0) {
            ticket--;
            System.out.println(Thread.currentThread().getName() + "---卖出的是第" + (ticket + 1) + "张, 剩余" + ticket + "张");
        }
    }
}


public class UseSellTicket {
    public static void main(String[] args) {
        // 创建卖票的任务
        SellTicket st = new SellTicket();
        // 把任务绑定到三个卖票窗口上, 也就三个窗口共同完成一个任务
        Thread t1 = new Thread(st, "窗口1");
        Thread t2 = new Thread(st, "窗口2");
        Thread t3 = new Thread(st, "窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}
(3)两种同步方法的锁对象
  • 成员同步方法的锁对象是:this
  • 静态同步方法的锁对象是:类名.class

3、Lock锁

(1)概述

虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock,Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化。

(2)使用方法

ReentrantLock构造方法

方法名说明
ReentrantLock()创建一个ReentrantLock的实例

加锁解锁方法

方法名说明
void lock()获得锁
void unlock()释放锁

代码示例

public class SellTicket implements Runnable {
    // 定义变量模拟要卖的100张票
    private int ticket = 100;
    // 创建一个锁对象属性
    private ReentrantLock l = new ReentrantLock();
   
     @Override
    public void run() {
        while (ticket > 0) {
            l.lock(); // 获取锁对象
            if (ticket > 0) {
                ticket--;
                System.out.println(Thread.currentThread().getName() + "---卖出的是第" +(ticket + 1) + "张, 剩余" + ticket + "张");
            }
            l.unlock();
        }
    }
}


// 线程安全问题演示
public class ThreadSafe {
    public static void main(String[] args) {
        // 卖票任务
        SellTicket st = new SellTicket();
        // 创建3个线程对象,模拟三个窗口
        Thread t1 = new Thread(st, "窗口1");
        Thread t2 = new Thread(st, "窗口2");
        Thread t3 = new Thread(st, "窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}

总结

无论使用哪种解决方案,原理都是要线程同步,要报原子性操作代码块的完整性,不能中途被打断,加锁同步。

加锁原则:保证锁唯一

五、死锁

(一)死锁的概述和现象演示

1、概述

线程死锁是指由于两个或者多个线程相互持有对方所需要的锁资源,导致这些线程处于等待状态,无法继续执行。

2、图示

3、代码示例

public class DeadLock {
// 两个锁对象
private static final String l1 = "A";
private static final String l2 = "B";

    public static void main(String[] args) {
        Thread t1 = new Thread("线程1") {
        @Override
        public void run() {
            synchronized (l1) {
                try {
                    Thread.sleep(20); // 休眠是为了放大死锁现象出现的概率,让t1和t2更容易交替执行
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (l2) {
                    System.out.println("任务1");
                }
            }
        }
    };
    
    Thread t2 = new Thread("线程2") {
        @Override
        public void run() {
            synchronized (l2) {
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (l1) {
                    System.out.println("任务2");
                }
            }
        }
    };
    t1.start();
    t2.start();
    }
}

(二)死锁诊断

六、线程状态

1、概述

线程生命周期,线程对象从生到死一个过程,当线程被创建并启动以后,他既不是一启动就进入了执行状态,也不是一直处于执行状态。线程对象在不同的时期有不同的状态。

2、线程状态图示

通过java代码的方式。获取实际中线程在程序中会出现的各种状态。在Thread类中提供有一个方法:getState()。此方法就可以返回线程对象状态。 

线程状态具体含义
NEW
一个尚未启动的线程的状态。也称之为初始状态、开始状态。
线程刚被创建,但是并未启动。还没调用start方法。MyThread t = new MyThread()只有线程对象,没有启动线程。
RUNNABLE
当我们调用线程对象的start方法,CPU执行线程中任务的时候。那么此时线程对象进入了RUNNABLE状态。
BLOCKED
当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入BLOCKED状态;当该线程持有锁时,该线程将变成Runnable状态。
WAITING
一个正在等待的线程的状态。也称之为等待状态。造成线程等待的原因有两种,分别是调用Object.wait()、join()方法。处于等待状态的线程,正在等待其他线程去执行一个特定的操作。例如:因为wait()而等待的线程正在等待另一个线程去调用notify()或notifyAll();一个因为join()而等待的线程正在等待另一个线程结束。
TIMED_WAITING
一个在限定时间内等待的线程的状态。也称之为限时等待状态。造成线程限时等待状态的原因有三种,分别是:
Thread.sleep(long),Object.wait(long)、join(long)。
TERMINSTED
一个完全运行完成的线程的状态。也称之为终止状态、结束状态

七、线程池

(一)线程池概述

提到池,大家应该能想到的就是水池。水池就是一个容器,在该容器中存储了很多的水。那么什么是线程池呢?线程池也是可以看做成一个池子 ( 容器 ) ,在该池子中存储很多个线程。一个装着多条线程对象的容器就是线程池

1、线程池存在的意义:

  • 系统创建一个线程的成本是比较高的,因为它涉及到与操作系统交互,当程序中需要创建大量生存期很短暂的线程时,频繁的创建和销毁线程对系统的资源消耗有可能大于业务处理是对系统的消耗
  • 系统资源的消耗很多,大于任务消耗的资源,这样就有点"舍本逐末"了。针对这一种情况,为了提高性能,我们就可以采用线程池。线程池在启动的时,会创建大量空闲线程,当我们向线程池提交任务的时,线程池就会找到一个空闲线程来执行该任务。等待任务执行完毕以后,线程并不会死亡,而是再次放回到线程池中称为空闲状态。等待下一次任务的执行。这样就可以达到对线程对象重复利用的效果

2、线程池缺点

如果长时间没有任务执行 线程池依然要保存这些线程对象 这些线程对象也要占用内存资源 就会对内存资源造成浪费

(二)Executors获取线程池

1、概述

JDK对线程池也进行了相关的实现,在真实企业开发中我们也很少去自定义线程池,而是使用JDK中自带的线程池。

2、理解

提前准备好一定数量的线程对象的一个容器 --- 得到线程池就相当于得到了线程对象
我们可以使用Executors中所提供的静态方法来创建线程池
(1)static ExecutorService newCachedThreadPool() 创建一个默认的线程池,只要你又要执行的任务,且池子中没有空闲线程,就会创建新的线程执行任务。
(2)static ExecutorService newFixedThreadPool(int nThread) 创建一个指定最多线程数量的线程池, 创建一个拥有固定个数线程的线程池。
(3)static ExecutorService newSingleThreadExecutor() 创建只有一个线程对象的线 程池。

3、ExecutorService

(1)使用:
1、准备线程任务【Runnable Callable&FutureTask Callable】
2、把线程任务提交给线程池【自动委派内部的线程对象来执行】
(2)功能:

submit(Runnable r):提交线程任务到线程池执行

submit(Callable c):提交线程任务到线程池执行线程池一般是不会关闭了,因为要复用这个线程池管理线程对象,线程池不关闭程序不会停止.

shutdown():关闭线程池

shutdownNow():关闭线程池

(3)代码示例

newCachedThreadPool()获取线程池

public static void reuseThread() {
    // Executors是java提供一个工具类,通过该类就可以获取java中给我们准备一些
    // 不同作用和特点线程池对象.
    // 获取一个能缓存线程对象的线程池.
    ExecutorService pool = Executors.newCachedThreadPool();
    for (int i = 1; i <= 100; i++) {
        // Runnable的run方法中的内容将来要在别的线程中执行.变量i是在主线程中定义的.变量的使用是不能夸线程的.
        // 之所不让跨线程使用的原因.多个线程操作同一个数据有安全隐患.
        final int j = i;
        Runnable r = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"----任务"+j);
            }
        };
        pool.submit(r);
    }
}



public static void simpleThreadPool1() {
    // 获取一个能缓存线程对象的线程池.
    ExecutorService pool = Executors.newCachedThreadPool();
    // 准备三个任务
    Runnable r1 = new Runnable() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+"----任务1"); // pool-1-thread-1----任务1
        }
    };
    Runnable r2 = new Runnable() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+"----任务2"); // pool-1-thread-2----任务2
        }
    };
    Runnable r3 = new Runnable() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+"----任务3"); // pool-1-thread-3----任务3
        }
    };

    // 任务提交到线程池
    pool.submit(r1);
    pool.submit(r2);
    pool.submit(r3);
    // 线程池不关闭程序不会停止.
}

newFixedThreadPool(int Thread)获取线程池

public static void useNewFixedThreadPool() {
    // Executors是java提供一个工具类,通过该类就可以获取java中给我们准备一些
    // 不同作用和特点线程池对象.
    // 获取一个能缓存线程对象的线程池.
    ExecutorService pool = Executors.newFixedThreadPool(3);

    Runnable r1 = new Runnable() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+"----任务1");
        }
    };

    Runnable r2 = new Runnable() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+"----任务2");
        }
    };

    Runnable r3 = new Runnable() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+"----任务3");
        }
    };

    Runnable r4 = new Runnable() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+"----任务4");
        }
    };
    pool.submit(r1);
    pool.submit(r2);
    pool.submit(r3);
    pool.submit(r4);
}

newSingleThreadExecutor()获取线程池 

public static void useNewSingleThreadExector() {
    // 获取只有一个线程对象线程池
    ExecutorService pool = Executors.newSingleThreadExecutor();
    Runnable r1 = new Runnable() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+"----任务1");
        }
    };
    Runnable r2 = new Runnable() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+"----任务2");
        }
    };
    pool.submit(r1);
    pool.submit(r2);
}

4、shutdown和shutdownNow方法

使用这个两个方法原因:一旦程序使用线程池,不关闭池子的话,程序就不会停止.只有关闭池子程序才会停止。
shutdown(): 关闭线程池
shutdownNow():关闭线程池
这两个方法共同点,都是把线程池关闭。
shutdown方法关闭池子的时候,如果任务已经提交了,但是没有执行到。任务处于等待有空闲线程状态。此时方法关闭池子能把处于等待状态任务给执行了。
shutdownNow方法关闭池子的时候,如果这个任务正在执行,保证任务执行完按,但是已经提交了,处于等待状态的任务,就不再执行了。
  • 21
    点赞
  • 58
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

互联网底层民工

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值