基本概念: 程序、进程、线程
程序: 是为了完成特定的任务、用某种语言编写的一组指令的集合。即指一段静态的代码段,静态对象
进程: 是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程: 有它自身的产生、存在和消亡的过程。 — — 生命周期
进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域(方法区和堆空间)
线程: 进程可以进一步的细化成线程,是一个程序内部的一条执行路径。
若是一个进程同一时间并行执行多个线程,就是支持多线程的
线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器,程序切换的开销小(虚拟机栈和程序计数器)
一个进程中的多个线程共享相同的内存单元/内存地址空间 -》他们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源就会带来安全的隐患(也就是我们常说的线程安全的问题)。
单核CPU和多核CPU的理解
单核CPU,其实是一种假设的多线程,因为在一个时间单元内,也只能执行一个线程的任务。例如: 虽然有多车道,但是收费站只有一个工作人员在收费,只有收了费才能通过,那么CPU就好比收费人员。如果有某个人不想去交钱,那么收费人员可以把他"挂起"。但是因为CPU时间单元特别短,因此感觉不出来。
如果是多核的话,才能更好的发挥多线程的效率。(现在服务器都是多核的)
一个java应用程序java.exe,其实至少有三个线程: main()主线程,gc() 垃圾回收线程,异常处理线程。当然如果发生了异常,会影响主线程。
并行与并发
并行: 多个CPU同时执行多个任务。比如: 多个人同时做不同的事
**并发:**一个CPU(采用时间片)同时执行多个任务。比如: 秒杀、多个人做同一件事
线程的创建和使用
java语言的jvm允许程序运行多个线程,它通过java.lang.Thread类来体现。
Thread类的特性:
每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主体称为线程体
通过该Thread对象的start()方法来启动这个线程,而非直接调用run()
/**
* 多线程的创建,方式一: 继承于Thread类
* 1. 创建一个继承于Thread类的子类
* 2. 重写Thread类的run()方法
* 3. 创建Thread类的子类对象
* 4. 通过此对象调用start()
*
* 例子: 遍历100以内所有的偶数
*
* @author xiong.zhang04@hand-china.com
* @date 2020-12-07
*/
public class ThreadTest{
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
System.out.println("hello");// 可以替换成一个执行时间消耗和子线程差不多的程序
for (int i = 0; i < 100; i++){
if (i % 2 != 0){
System.out.println(i + "*******main()*******");
}
}
}
}
// 附加一个额外知识点: 获取当前线程的名称: Thread.currentThread().getName()
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++){
if (i % 2 == 0){
System.out.println(i);
}
}
}
}
// 这样一来就能看到子线程和主线程之间是具有一个交互性的,就是两个结果输出的混在了一起。
两个特殊问题的说明
start():
启动当前线程
调用当前线程的run()
所以我们不能直接通过调用run()方法的方式来调用线程,这样属于在同一线程内执行了一下run()方法而已,没有多线程。
问题二: 再启动一个线程,遍历一百以内的奇数我们可不可以再t1.start()下方再写一遍?
不可以,每个线程都有一个threadstatus值,一旦这个线程启动了这个值就不为0了,那么就不能再次执行了。
那我们该怎么去重启一个新的线程呢?
新建一个对象。
创建Thread类的匿名子类
public class TreadDemo{
public static void mian(String[] args){
new Thread(){
@Override
public void run(){
}
}.start()
}
}
Thread类的有关方法
void start(): 启动线程,并执行对象的run()方法
run():线程在被调度时执行的操作
String getName(): 返回线程的名称
void setName(String name): 设置该线程的名称
static Thread currentThread():返回当前线程。在Thread子类中就是this,通常用于主线程和Runnable实现类。
yield() 礼让,当前线程让出执行权,但是可能会再次被分配到当前线程上来
join() 插队,在线程a中调用线程b的join方法,线程a进入阻塞状态,直到线程b完全执行完以后。
stop():强制线程生命周期结束,不推荐使用
sleep(long millitime): 让线程进入阻塞状态
boolean isAlive(): 返回boolean,判断线程是否还活着
public class helloThread extends Thread {
public static void main(String[] args) {
// 这样是通过构造器的方法给线程命名了。
ThreadMethodTest threadMethodTest = new ThreadMethodTest("Thread - 1:");
//ThreadMethodTest threadMethodTest = new ThreadMethodTest();
// 设置线程的名字
threadMethodTest.setName("线程1");
threadMethodTest.start();
// 给主线程命名
Thread.currentThread().setName("主线程");
for (int i = 0; i < 100; i++) {
if (i % 2 != 0) {
// 获取线程一的名字
System.out.println(Thread.currentThread().getName() + ":" + i);
}
// if(i == 20){
// try{h1.join();}catch(InterruptedException e){e.printStackTrace();}
}
}
}
}
class ThreadMethodTest extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
// 获取线程一的名字
System.out.println(Thread.currentThread().getName() + ":" + i);
}
//if(i %20 == 0) {
// // 释放当前cpu的执行权,礼让
// yield();
//}
}
}
public ThreadMethodTest(String name){
super(name);
}
}
线程的优先级
抢占式: 高优先级的线程抢占CPU
Java的调度方式:
同优先级线程组成先进先出队列(先到先服务),使用时间片策略
对高优先级,使用优先调度的抢占式策略
线程优先级:
MAX_PRIORITY: 10
MIN_PRIORITY: 1
NORM_PRIORITY: 5
涉及方法:
getPriority();返回线程优先值
setPriority(int newPriority) 改变线程优先级
说明:
线程创建时继承父线程的优先级
低优先级只是获取调度的概率低,并非一定是在高优先级线程之后才被调用
例题
class Window extends Thread{
// 这里为什么要加static? 考虑一下加了能不能解决这个问题?运行代码之后仔细思考一下
private static int ticket = 100;
@Override
public void run(){
while(true){
if(ticket > 0){
System.out.println(getName() + ":卖票,票号为" + ticket);
ticket--;
} else {
break;
}
}
}
}
public class WindowTest{
public static void main(String[] args) {
Window t1 = new Window();
Window t2 = new Window();
Window t3 = new Window();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
创建多线程的方式二: 实现Runnable接口
- 创建一个实现Runnable接口的类
- 实现类去实现Runnable中的抽象方法: run()
- 创建实现类的对象
- 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
- 通过Thread类的对象,去调用start()方法
class Mthread implements Runnable{
@Override
public void run() {
for(int i = 0; i < 100; i++){
if(i % 2 == 0){
System.out.println(i);
}
}
}
}
public class ThreadTest1 {
public static void main(String[] args){
// 创建实现类的对象
Mthread mThread = new Mthread();
// 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
Thread t1 = new Thread(mThread);
// 通过Thread类的对象调用start():
// 调用runnable类型的target和之前的那种方法不是一回事,这个是在使用有参构造器的时候
// 把mThread作为参数传递给了target属性,它是一个runnable类型的变量,Thread方法里面写的
// run方法是,当target不为null时,就调用target的run方法。
t1.start();
// 再启动线程
}
}
// 代码里面看的Thread类是实现Runnable的类,在Thread中实现了run方法
使用runnable接口实现的方式来实现卖票
class window1 implements Runnable{
private int ticket = 100;
@Override
public void run() {
while(true){
if(ticket > 0){
System.out.println(Thread.currentThread().getName() + ": 卖票,票号为: " + tickets);
tickets--;
} else{
break;
}
}
}
}
public class WindowTest1 {
public static void main(String[] args) {
Window1 w = new Window1();
// 目前来看,三者公用同一个对象,所以就可以不去考虑那个static的问题。
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
两种创建方式的对比
开发中:优先选择runnable接口的方式,
联系: Thread类是Runnable接口类的实现
原因:
- 实现的方式没有类的单继承性的局限性
- 实现的方式更加适合用来处理多个线程有共享数据的情况
相同点: 两种方式都需要重写run(),将线程要执行的逻辑声明在run()中。
线程的生命周期
JDK中用Thread.State(内部类)类定义了线程的几种状态
要想实现多线程,必须在主线程中创建新的线程对象。java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下五个状态:
- 新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
- 就绪: 处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行条件,只是没有分配到CPU资源
- 运行: 当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能
- 阻塞: 在某种特殊情况下,被人为挂起或执行输入输出操作时,让出cpu并临时中止自己的执行,进入阻塞状态
- 死亡: 线程完成了它的全部工作或线程被提前强制性的中止或出现异常导致结束
线程的同步
上面的例子虽然效果大致实现,但是,有一个问题,就是多线程引起了线程之间的资源争夺,比如上方例子中的票数,就是问题的体现。
出现的原因: 当某个线程操作车票的过程中,尚未完成操作,其他线程参与进来了,也开始了车票,这就导致,可能出现重复数据或者越界的问题。
同步代码块
使用同步代码块解决runnable实现类
synchronized(同步监视器){
// 需要被同步的代码
}
说明: 操作共享数据的代码,即为需要被同步的代码
共享数据: 多个线程共同操作的变量。比如上方的ticket
同步监视器: 俗称(锁)。任何一个类的对象,都可以充当锁。使用起来要求多线程之间必须要同一把锁
同步的方式,解决了线程的安全问题。 — 好处
操作同步代码块时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。
class window1 implements Runnable{
private int ticket = 100;
// 任意一个类的对象
Object obj = new Object();
@Override
public void run() {
while(true){
// 使用到同一资源的代码块上锁
synchronized(obj) {
if(ticket > 0){
System.out.println(Thread.currentThread().getName() + ": 卖票,票号为: " + ticket);
ticket--;
} else{
break;
}
}
}
}
}
public class WindowTest1 {
public static void main(String[] args) {
Window1 w = new Window1();
// 目前来看,三者公用同一个对象,所以就可以不去考虑那个static的问题。
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
// 其实这里去new一个新的对象比较蠢,可以直接用this 因为对象还是只有一个呀
使用同步代码块解决继承Thread类的方式的线程安全问题
先看看实现代码
class Window extends Thread{
private static int ticket = 100;
// 考虑为什么要加static
private static Object obj = new Object();
@Override
public void run(){
while(true){
// 锁!
synchronized(obj){
if(ticket > 0){
System.out.println(getName() + ":卖票,票号为" + ticket);
ticket--;
} else {
break;
}
}
}
}
}
public class WindowTest{
public static void main(String[] args) {
Window t1 = new Window();
Window t2 = new Window();
Window t3 = new Window();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
我们上方最后说,实现runnable的加锁方式可以改成this,那我们这里可不可以也改成this呢?不能。因为是不同的对象,this每次都不同。
其实这里有一种替换方式,每个类其实加载之后它也是一个对象,类只会加载一次,我们可以通过类.class的方式加载成自己这个类的对象。
同步方法
使用同步方法解决实现Runnable线程安全问题
如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明为同步的。
class window1 implements Runnable{
private int ticket = 100;
// 任意一个类的对象
Object obj = new Object();
@Override
public void run() {
while(true){
// 这样一来就给使用这段代码的show方法直接加锁了,这样是我们需要介绍的同步方法。
show();
// 这里有个问题,show()方法里面写的break,在这个while true里面不会生效,
}
}
private synchronized void show(){
if(ticket > 0){
System.out.println(Thread.currentThread().getName() + ": 卖票,票号为: " + ticket);
ticket--;
} else{
break;
}
}
}
public class WindowTest1 {
public static void main(String[] args) {
Window1 w = new Window1(
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
使用同步方法解决继承Thread类的线程安全问题
先考虑一下,也使用类似上方的实现Runnable的方法直接把操作公共变量部分提取出来然后给方法加锁可行吗?
class Window extends Thread{
private static int ticket = 100;
// 考虑为什么要加static
private static Object obj = new Object();
@Override
public void run(){
while(true){
show();
}
}
private synchronized void show(){
if(ticket > 0){
System.out.println(Thread.currentThread().getName() + ": 卖票,票号为: " + ticket);
ticket--;
} else{
break;
}
}
}
public class WindowTest{
public static void main(String[] args) {
Window t1 = new Window();
Window t2 = new Window();
Window t3 = new Window();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
运行之后可以看到不可行,为啥呢?因为每个对象操作的都是自己的方法每个方法的this都不同,也就是说,同步方法是使用的this来作为锁的,显然,多个线程就造成了多个锁,这显然不行。那咋办?
有办法的,给这个方法改成static的。让同步监视器变成唯一的。
单例模式之懒汉模式
复习一下懒汉模式,我们说,懒汉是需要再去造对象,那么再回忆一下单例模式怎么写
- 私有化构造器
- 内部创建类的对象
- 提供静态方法供外部获取
class Bank{
private Bank(){}
private static Bank instance = null;
public static Bank getInstance(){
if (instance == null){
instance = new Bank();
return instance;
}
return instance;
}
}
public class BankTest {
}
class Bank{
private Bank(){}
private static Bank instance = null;
// 改装之后加上synchronized
public static synchronized Bank getInstance(){
if(instance == null) {
instance = new Bank();
return instance;
}
return instance;
}
}
加上了synchronized之后,getInstance之后,这个类就安全了,他的锁就是当前类本身。这种是同步方法的方式,那么同步代码块怎么做呢?
class Bank{
private Bank(){}
private static Bank instance = null;
// 改装之后加上synchronized
public static Bank getInstance(){
// 这个效率差,为啥呢,就是代码实际运行到这个里面来了,到这个代码块的时候,才发现被锁了就全在这里等了
synchronized (Bank.class) {
if(instance == null) {
instance = new Bank();
return instance;
}
return instance;
}
}
// 改成这样
public static Bank getInstance1(){
if(instance == null){
synchronzied(Bank.class) {
if(instance == null){
instance = new Bank();
}
}
}
return instance;
}
}
死锁问题
死锁: 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
出现死锁之后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续,我们构造一个死锁
public class ThreadtTest {
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
// 创建匿名对象
new Thread(){
@Override
public viod run(){
synchronized(s1) {
s1.append("a");
s2.append("1");
synchronized(s2){
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s1);
}
}
}
}.start();
new Thread(new Runnable(){
@Override
public void run(){
synchronized(s2) {
s1.append("c");
s2.append("3");
synchronized(s1){
s1.append("d");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}
}).start();
}
}
很明显的一个问题,如果上面的线程锁住s1,还未到synchronized(s2)这行的时候,这个时候cpu到第二个线程了,他进来锁住了s2,这个时候,上面线程等着下方释放s2,下方线程等着上方线程释放s1,这时候就出现死锁了
同步锁
从JDK5.0开始,java提供了更强大的线程同步机制,通过显式定义同步锁对象来实现同步.同步锁使用Lock对象充当.
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具.锁提供了对共享资源的独占空间,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应获得Lock对象.
ReentrantLock类实现了Lock,它拥有与sychronized相同的并发性和内存语意,在实现线程安全的控制中,比较常见的是ReentrantLock,可以显式加锁 和 释放锁
class Window implements Runnable{
private int ticket = 100;
// 1. 实例化Lock
private ReentrantLock lock = new ReentrantLock(true);
@Override
public void run(){
while(true){
try{
// 调用lock方法
lock.lock()
if(ticket > 0){
System.out.println(Thread.currentThred().getName() + ": 售票,票号为:" + ticket);
ticket--;
} else {
break;
}
} finally {
// 调用解锁方法
lock.unlock();
}
}
}
}
public class LockTest{
public static void main(String[] args){
Window w = new Window();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
synchronized与Lock的异同
相同点: 都是用来解决线程安全的
不同点: synchronized机制在执行相同的同步代码之后,自动释放同步监视器
Lock需要手动的启动同步(lock()),同时结束也需要手动释放(unlock())
线程的通信
notify()和notifyall()唤醒线程
wait()让线程进入阻塞状态,但是会释放锁。
说明: 这个只能出现同步代码块或同步方法中。且调用者必须是同步监视器,这三个方法都是定义在Object类中的
总结
sleep方法和wait方法的异同
相同点,一旦执行方法,都可以使得当前线程进入阻塞状态
不同点: 两个方法声明位置不同,Thread声明的sleep Object声明的wait()
调用的要求不同,sleep可以在任何需要的场景下调用。wait必须在同步代码块或同步方法 中调用
sleep不释放锁,wait释放锁。
生产者/消费者的问题
生成者将产品教给店员,而消费者从店员处取走产品,店员一次只能持有固定数量的产品,如果生产者试图生产更多产品,店员会叫生产者停下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会让消费者等一下,如果店中有了再通知消费者取走产品
可能会出现两个问题:
生产者比消费者快,消费者会漏掉一些信息取不到
消费者比生产者快,消费者会取相同数据
/**
* 1.是否有多线程? 是
* 2. 是否有共享数据? 是
* 3.如何解决线程安全? 同步机制,有三种方式
* 4. 是否涉及线程通信? 是
*/
class Clerk{
private int productProduct = 0;
public synchronized void produceProduct(){// 生产产品
if(productProduct < 20) {
productProduct++;
System.out.println(Thread.currentThread().getName() + ":开始生产第" + productProduct + "个产品");
// 这里的逻辑里面可以加上,每次生产了,就去通知消费者唤醒消费者的线程
notify();
} else {
try{
wait();
} catch (InterruptedException e){
e.printStackTrace();
}
}
}
public synchronized void consumeProduct(){// 消费产品
if(productProduct > 0) {
System.out.println(Thread.currentThread().getName() + ":开始生产第" + productProduct + "个产品");
productProduct--;
// 这里逻辑需要,每次消费一个产品,需要去唤醒生产者,去生产
notify();
} else {
try{
wait();
} catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
class Producer extends Thread{ // 生产者
private Clerk clerk;
public Producer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run(){
System.out.println(getName() + ":开始生产产品");
while(true){
try{
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.produceProduct();
}
}
}
class Consumer extends Thread{ // 消费者
private Clerk clerk;
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run(){
System.out.println(getName() + ":开始消费产品");
while(true){
try{
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.consumeProduct();
}
}
}
public class ProductTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
// 新建一个生产者
Producer producer = new Producer(clerk);
p1.setName("生产者1");
// 新建一个消费者
Consumer c1 = new Consumer(clerk);
c1.setName("消费者1");
p1.start();
c1.start();
}
}
JDK5.0新增线程创建方式
实现Callable接口
与Runnable相比,Callable功能更加强大
相比run()方法,可以有返回值
方法可以抛出异常
支持泛型的返回值
需要借助FutureTask类,比如获取返回值
class NumThread implements Callable{
@Override
public Object call() throws Exception {
// 相当于我们的run()方法一样,但是它可以抛异常
for(int i = 0; i < 100; i++){
if(i%2 == 0){
System.out.println(i);
sum += i;
}
}
// 注意这里有装箱操作哦
return sum;
}
}
public class ThreadNew{
publi static void main(String[] args){
NumThread numThread = new NumThread();
// 通过FutureTa来获取结果:
// 先说一下Future接口:
// 可以对具体的Runable、Callable任务的执行结果进行取消、查询是否完成、获取结果等
// FutureTask是Futrue是接口的唯一的实现类
// FutureTask同时实现了Runnable、Future接口。它既可以作为Runable被线程执行,又可以作为
// Future得到Callable的返回值
FutureTask futureTask = new FutureTask(numThread);
// 调用start方法启动
new Thread(futureTask).start();
try{
// get方法即为FutureTask构造器参数Callable实现类的重写的回调方法call()的返回值。
Object sum = futureTask.get();
System.out.println(sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
使用线程池
经常创建和销毁,对资源损耗比较大,可以提前创建好多个线程,放入线程池中,使用时直接提供,使用完放回池中,实现反复利用。
好处:
提高了响应速度(减少了创建新线程的时间)
降低资源消耗(重复利用线程池中线程,不需要每次都创建)
便于线程管理
corePoolSize:核心池的大小
maximumPoolSize: 最大线程数
keepAliveTime: 线程没有任务时最多保持多长时间会终止
JDK5起提供了线程池的API: ExecutorService和Executors
ExecutorService: 真正的线程池的接口。常见的子类ThreadPoolExecutor
Executors: 工具类、线程池的工厂类,用于创建并返回不同类型的线程池
class NumberThread implements Runnable{
@Override
public void run(){
for(int i = 0;i <= 100; i++){
if(i % 2 == 0){
System.out.println(i);
}
}
}
}
public class ThreadPool{
public static void main(String[] args) {
// 创建池
ExecutorService service = Executors.newFixedThreadPool(10);
// 考虑进行强转,为什么,因为上方返回的是一个接口类型的值,接口里面都是常量,不转怎么设置哩
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
// 设置线程池属性
service1.setCorePoolSize(15);
service1.setKeepAliveTime();
// 执行指定的线程操作。需要提供实现Runnable接口或Callable接口实现类的对象
service.execute(new NumberThread());// 适合适用于Runnable
// service.sumbit(Callable callable); // 使用Callable
// 关闭连接池
service.shutdown();
}
}