一、概念
1.进程
在面向进程设计的计算机结构中,进程是程序的基本执行实体;在向线程设计的计算机结构中,进程是线程的容器。我们打开任务管理器,会看到电脑当前运行的进程,一个进程可以监听多个端口号pid(端口号),通过端口号进行网络数据的传输。我们看到电脑同事可以执行多个程序,并不是cpu在同时运行几个程序,而是在多个进程之间快速的切换,在我们肉眼看来是在同步执行,多核cpu在进程之间的切换上更加高效,因此电脑性能也就越高,但高频率的cpu切换在制作工艺和费用上也会越难越高,内存也会成为一个瓶颈,可以学习多核编程进行了解。
2.线程
线程是我们执行程序的最小单元。线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。我们java虚拟机的启动至少有两个线程的执行,一个是程序主线程,另外一个是jvm的垃圾回收线程。
二、java多线程创建的方式
java对线程的描述是Thread类,创建多线程有两种方式:
一是继承Thread 类,复写Thread的run方法(run方法时存储线程要运行的代码内容),调用线程的start方法(启动线程,调用线程的run方法,如果直接调用run()方法,他就是普通的对象调用方法,并没有运行线程)。
二是声明实现一个Runnable接口的类,该接口里面只有一个run方法,并且为非Thread子类提供了一种激活方式(步骤:定义实现Runnable的接口;覆盖Runnable中的run方法;通过Thread创建线程对象;Runnable接口实现类作为参数传到Thread的构造函数中;调用thread对象的start方法启动线程 )。在创建Thread对象的时候,就指定run方法所属的对象,我们看Thread中有一个参数为Runnable的构造方法,就是专门用来接收runnable对象的。如下代码,我们创建一个实现Runnable的类,再调用它。
实现方式和集成方式的区别:
1.避免了单继承的局限性,建议使用这种方式:集成的方式在继承Thread后,不能再集成其他的类,比如我们有些子类在继承了父类之后,有些方法时需要多线程操作,这时就无法再集成Thread类了,此时就可以通过实现Runnable接口,实现run方法。
2.线程代码存放位置不一样:继承Thread线程代码存放在Thread子类的run方法中,实现Runnable代码在接口子类的run方法中。
三、java线程自带的一些方法
- run(): 通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
- currentThread():静态方法,返回执行当前代码的线程
- getName():获取当前线程的名字
- setName():设置当前线程的名字
- yield():释放当前cpu的执行权
- join():在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态。
- stop():已过时。当执行此方法时,强制结束当前线程。
- sleep(long millitime):让当前线程“睡眠”指定的millitime毫秒。在指定的millitime毫秒时间内,当前线程是阻塞状态。
- isAlive():判断当前线程是否存活
四、多线程的安全问题
计算机系统资源分配的单位为进程,同一个进程中允许多个线程并发执行,一个线程的执行权还没结束,可能执行权就被抢走,并且多个线程会共享进程范围内的资源:例如内存地址。当多个线程并发访问同一个内存地址并且内存地址保存的值是可变的时候可能会发生线程安全问题。
解决办法:对多条操作数据共享的语句,让一个线程都执行完,其他线程不参与执行(同步代码块);synchronized(对象){ 需要被同步的代码 } 这里的对象如同锁,持有锁的线程可以在同步中执行,没有持有的线程即使获取cpu的执行权,也无法执行。同步的前提①多线程才需要同步;②必须适合多个线程使用同一个锁,也就是同一个对象。同步虽然解决了安全问题,但是在每次运行之前都会先判断一次锁,会消耗资源(弊端)。
写完多线程,如何考虑是否存在线程安全问题:
1.明确哪些代码是多线程运行代码,主要包括run方法和里面调用的方法;
2.明确哪些是共享数据;
3.明确多线程运行代码中哪些语句是操作共享数据的;
一般,我们可以在操作共享数据操作下,让线程睡眠一下,Thread.sleep() 这样就很容易暴露出。
同步函数:还可以让函数具有同步性,也是通过synchronized定义;
同步代码块的写法:
同步函数的写法:方法的返回类型为synchronized。同步函数用的锁,他能用的锁就是this,调用同步函数的那个对象。如果同步函数被静态修饰后,使用的锁是该方法所在类的字节码文件对象,类名.class。
懒汉式单例在多线程中的使用(因为懒汉式是延迟加载,在多线程中中调用每个线程都会创建一个实例,为了避免,所以要使用锁的机制,通过双重判断,提高效率):
//懒汉式,实例的延迟加载
public class Single {
private static Single single = null;
private Single() {
}
public static Single getSingle() {
if (single == null) {
synchronized (Single.class) {
if (single == null) {
single = new Single();
}
}
}
return single;
}
}
死锁现象:一般出现在同步中嵌套同步,锁又不同。
产生死锁的四个条件
1.互斥条件:所谓互斥就是进程在某一时间内独占资源。
2.请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
3.不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
4.循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
五、线程的几种状态
1.创建: new一个thread子类对象;
2.运行: start() 方法的调用;
3.冻结(睡眠/等待):sleep() 方法的调用转为睡眠状态,时间到后,自动转为临时状态,再到运行状态;wait()方法调用转为等待状态,不会自动转换为运行,要调用notify()唤醒方法;冻结状态,线程放弃了执行权,cpu不会执行该线程,当线程从冻结状态唤醒之后,不是立刻到运行状态,而是先到阻塞状态,获得执行权,才到运行状态;
4.销毁:线程运行过程中挂掉或者冻结过程中挂掉,都会让线程处于销毁状态,还有一种是当线程没有执行的内容了,run()结束,线程也会被销毁;
5.阻塞:线程被start()之后,不一定被运行,因为cup在某一时刻只能执行一个线程,其他线程就处于阻塞状态,等待cpu的执行权,该线程具备运行的资格,但不具备执行权,叫做阻塞状态。
六、多线程通信
多个线程对同一个资源的操作不一致,一个写,一个取,对于资源的情况,他们需要实时的通信。
需求描述:实现一个资源类,一个写入姓名和性别,然后读出姓名和性别;
资源类:
@Data
public class Resource {
private String name;
private String sex;
}
输入类:
public class ResInput implements Runnable {
private Resource resource;
public ResInput(Resource resource) {
this.resource = resource;
}
public void run() {
int x = 0;
while (true) {
if (x == 0) {
resource.setName("mike");
resource.setSex("男");
} else {
resource.setName("lili");
resource.setSex("女");
}
x = (x + 1) % 2;
}
}
}
输出类:
public class ResOutput implements Runnable {
private Resource resource;
public ResOutput(Resource resource) {
this.resource = resource;
}
public void run() {
while (true) {
synchronized (resource) {
System.out.print(resource.getName() + "==" + resource.getSex()+"\n");
}
}
}
}
运行结果:
问题1:出现姓名和性别的混乱,这时候因为传入姓名时,读的线程就会抢的cpu执行权,还没待性别写入,就读出了;
解决办法:通过同步解决:
1.是不是多个线程;
2.对资源操作的程序加同步;
3.是不是同一个锁;(使用同一个资源对象)
输出结果:发现不存在资源性别混乱的问题了。
问题2:但是因为线程切换的问题,导致一片片的执行写和读,并没有达到我们需求要求——写入之后读取。
解决办法:在资源中加入标志,是否满足取的条件。通过wait,让线程冻结,等待条件满足。等待唤醒机制
注意:只有同一个锁上的被等待线程,可以被同一个锁上的notify唤醒,不可以对不同锁中的线程进行唤醒。也就是说等待和唤醒都要是同一个锁。wait() 和notify() 其实都是定义在Object中的。
写进程:
public class ResInput implements Runnable {
private Resource resource;
public ResInput(Resource resource) {
this.resource = resource;
}
public void run() {
int x = 0;
System.out.print(x+"=======================\n");
while (true) {
synchronized (resource) {
if(resource.flag){
try {
resource.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
if (x == 0) {
resource.setName("mike");
resource.setSex("男");
} else {
resource.setName("lili");
resource.setSex("女");
}
x = (x + 1) % 2;
resource.flag = true;
resource.notify();
}
}
}
}
}
读进程:
public class ResOutput implements Runnable {
private Resource resource;
public ResOutput(Resource resource) {
this.resource = resource;
}
public void run() {
while (true) {
synchronized (resource) {
if(!resource.flag){
try {
resource.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
System.out.print(resource.getName() + "==" + resource.getSex()+"\n");
resource.flag = false;
resource.notify();
}
}
}
}
}
运行结果:
生成者和消费者问题
- 一个生产者 一个消费者:
MyResource.class 资源
public class MyResource {
private String name;
private int count = 1;
private Boolean flag = false;
public synchronized void set(String name) {
if (flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.name = name + "---" + count++;
System.out.print(Thread.currentThread().getName() + "...生产者..." + this.name + "\n");
flag = true;
this.notify();
}
public synchronized void out() {
if (!flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.print(Thread.currentThread().getName() + "============消费者..." + this.name + "\n");
flag = false;
this.notify();
}
}
MyProducer.class 生产者
public class MyProducer implements Runnable {
private MyResource myResource;
public MyProducer(MyResource myResource) {
this.myResource = myResource;
}
public void run() {
while (true){
myResource.set("+商品+");
}
}
}
MyConsumer.class 消费者
public class MyConsumer implements Runnable {
private MyResource myResource;
public MyConsumer(MyResource myResource) {
this.myResource = myResource;
}
public void run() {
while (true){
myResource.out();
}
}
}
通过主程序生成一个生产者和一个消费者。
public class ProducerAndConsumerMain {
public static void main(String[] args) {
MyResource myResource = new MyResource();
Thread thread01 = new Thread(new MyProducer(myResource));
Thread thread02 = new Thread(new MyConsumer(myResource));
thread01.start();
thread02.start();
}
}
运行结果,没有什么问题,生产之后就会被消费。
但是,如果是多个生产者和多个消费者同时在生产和消费,就会出现线程安全问题,导致生产和消费的数据紊乱的问题。如下是多个生产和消费者的运行结果。
代码改造:
public class MyResource {
private String name;
private int count = 1;
private Boolean flag = false;
public synchronized void set(String name) {
while (flag) {
//这是使用while当wait的线程被唤醒时,首先判断状态,
// 而不是之前的if阻塞后,唤醒之后不管什么状态继续执行下面的代码
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.name = name + "---" + count++;
System.out.print(Thread.currentThread().getName() + "...生产者..." + this.name + "\n");
flag = true;
this.notifyAll();
// notify唤醒只会唤醒一个,有可能是唤醒本方法中等待的,如t1 和 t2 都是等待中,
// t1判断flag为false,那他就唤醒等待现在中的一个,如果唤醒的是t2,继续生产,则没办法消费
//这里使用 notifyAll() 唤醒所有的,让唤醒的线程自己判断当前的状态是否满足要执行即可
}
public synchronized void out() {
while (!flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.print(Thread.currentThread().getName() + "============消费者..." + this.name + "\n");
flag = false;
this.notifyAll();
}
}
结果如下,生产和消费一一对应:
Lock
我们上面的生产者和消费者,多个生产和消费,通过notifyAll唤醒所有的线程,这个会把自己方法的线程也唤醒(也就是生产者线程进入等待之前,不仅会唤醒消费者中等待的线程,还会唤醒消费者中等待的其他线程),去抢夺资源,后来在java1.5之后,出现了显式的锁机制Lock。
Lock提供了比synchronized语句和方法可获得更广泛的锁操作。单个 Lock 可能与多个 Condition 对象关联。Condition 接口描述与锁有关联的条件变量,Condition将Object监视器方法(wait、notify、notifyAll)分解成截然不同的对象,以便通过将这些对象任意的Lock实现组合使用,为每个对象提供多个等待set,其中Lock替代了synchronized方法和语句,Condition替代了Object监视器方法的使用。
ClockResource.class
public class ClockResource {
private String name;
private int count;
private Boolean flag = false;
// 定义一个锁
private Lock lock = new ReentrantLock();
// 定义锁中的condition对象,一个生产者用,一个消费者用
private Condition condition_pro = lock.newCondition();
private Condition condition_con = lock.newCondition();
public void set(String name) throws InterruptedException {
lock.lock(); //上锁
try {
while (flag) {
// 标记为真时,生产者线程进入等待状态
condition_pro.await();
}
this.name = name + "---" + count++;
System.out.print(Thread.currentThread().getName() + "...生产者..." + this.name + "\n");
flag = true;
// 唤醒消费者中的等待的线程
condition_con.signalAll();
} finally {
lock.unlock(); //释放锁
}
}
public synchronized void out() throws InterruptedException {
lock.lock();
try {
while (!flag) {
// 消费者线程等待
condition_con.await();
}
System.out.print(Thread.currentThread().getName() + "============消费者..." + this.name + "\n");
flag = false;
// 把生产者线程唤醒
condition_pro.signalAll();
} finally {
lock.unlock();
}
}
}
七、线程的停止
stop() 方法已经过时。因为多线程的运行,运行代码通常是循环结构,只要控制住循环,就可以让run方法结束,也就是线程结束。但是有一种特殊情况,当线程处于冻结状态时,就不会读取到标记,线程就不会结束,只有让线程唤醒,重新读取运行的标记,做判断。
interrupt(): 在Thread中提供了一个方法,interrupt中断线程,如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。仅此而已。是清除线程的冻结状态,让线程从冻结状态强制恢复到运行状态,这样就可以操作标记,让线程结束
join():t.join()方法只会使主线程(或者说调用t.join()的线程)进入等待池并等待t线程执行完毕后才会被唤醒。并不影响同一时刻处在运行状态的其他线程。
八、守护线程
setDaemon() 设置线程为守护线程的方法,守护线程是一种特殊的线程,就和它的名字一样,它是系统的守护者,在后台默默完成一些系统性的服务,比如垃圾回收线程,JIT线程就可以理解为守护线程。与守护线程相对的是用户线程,用户线程可以认为是系统的工作线程,它会完成这个程序要完成的业务员操作。如果用户线程全部结束,则意味着这个程序无事可做。守护线程要守护的对象已经不存在了,那么整个应用程序就应该结束。
可以通过isDaemon()方法来判断是否是守护线程:如果返回false,
九、线程的优先级
java多线程的优先级在1-10之间的正整数,超出这个范围就会抛出IllegalArgumentException异常。线程的优先级越高并不意味着就一定被先执行,只是代表着获取cpu执行权的概率加大,在创建线程时,可以通过setPriority()来设置线程的优先级;java中线程的优先级具有继承性,如果线程A继承了线程B,则A和B的优先级一样。
线程的yield():让当前运行的线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。但很有可能又会被线程调度重新调用该线程,而达不到让步的效果。