10月21, 2020

JAVA多线程

进程:CPU为每个应用程序分配的独立空间,一个进程可能有多个线程。进程为线程中的摸个执行任务程序,多个进程之间可以进行共享数据。而JAVA的线程则是由JVM进程分配的,main方法所在线程,则成为主线程。

〇、线程状态

正常情况线程执行步骤:新建-》运行-》死亡

当CPU资源不够时,CUP分配给各个线程的资源可能不同(貌似有点像是线程在抢资源,实际是CPU分配资源给每个线程)。 因此就会出现线程的阻塞、休眠、等待3个状态。其中阻塞状态,当cpu资源够时,阻塞状态的线程可能恢复到运行状态,而休眠、等待的线程也可能进入运行状态。休眠、等待状态可能转换成阻塞状态,但是阻塞状态不会变成休眠、等待状态。

线程状态图.png

一、多线程的实现

0、线程常用方法
package learn.javase.threads;
/**
 * 继承Thread创建线程,设置线程名称、获取线程名称
 * @author Jole
 *
 */
public class ThreadDemo01 {

    public static void main(String[] args) {
        MyThreadsDemo mt = new MyThreadsDemo();
        mt.setName("Hi");
        mt.start();
        //获取当前线程
        Thread t = Thread.currentThread();
        System.out.println(t.getName());

    }

}

线程实现方式主测试类:

package learn.javase.threads;

public class MyThreadsDemo extends Thread{
    public MyThreadsDemo() {
        super("Google");
    }

    public void run() {
        for(int i=0;i<5;i++) {
            try {
                Thread.sleep(1000);
                System.out.println(i);
            }catch(Exception e) {
                System.out.println(e.getMessage());
            }

        }
    }
}

线程测试主类:

package learn.javase.threads;
/**
 * 实现多线程的4中方式:
 * 1、继承Thread类
 * 2、实现Runnable接口
 * 3、匿名内部类
 * 4、匿名接口
 * 5、实现Callable接口,与Runnable的区别:
 *   可以有返回值
 *   可以抛异常
 * @author Jole
 *
 */
public class ThreadNumberDemo {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        //方式1,继承Thread
        new ThreadThread1().start();

        //方式2,实现Runnable接口
        new Thread(new ThreadRunnables2()).start();

        //方式3,匿名内部类
        new Thread() {
            public void run() {
                System.out.println("匿名内部类,实现多线程");
            }
        }.start();

        //方式4,匿名接口
        Runnable r = new Runnable() {
            public void run() {
                System.out.println("匿名接口,实现多线程");
            }
        };
        new Thread(r).start();

        //方式4的简洁版
        new Thread(new Runnable() {
            public void run() {
                System.out.println("匿名接口简洁版,实现多线程");
            }
        }).start();
    }

}
1、继承Thread
package learn.javase.threads;
/**
 * 方式一继承Thread实现多线程
 * @author Jole
 *
 */
public class ThreadThread1 extends Thread{

    public void run() {
        System.out.println("extends ....thread");
    }
}
2、实现Runnable接口
package learn.javase.threads;
/**
 * 实现Runnable接口
 * @author Jole
 *
 */
public class ThreadRunnables2 implements Runnable{
    public void run() {
        System.out.println("runnable .. threads");
    }

}
3、实现Callable

通过使用线程池,实现Callable,主要用于有返回值和抛出异常的。如下的线程池实现。

4、区别

继承Thread接口与实现Runnable接口,实现多线程区别:

  • 1、单继承、多实现
  • 2、实现Runnable可以将线程与任务(run())解耦
package learn.javase.threads;
/**
 * 实现Runnable接口,创建线程。
 * 继承Thread接口与实现Runnable接口,实现多线程区别:
 * 1、单继承、多实现
 * 2、将线程与任务(run())解耦
 * @author Jole
 *
 */
public class RunnableDemo {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        new Thread(new RunnableThreadDemo()).start();
        for(int i=0;i<5;i++) {
            System.out.println("main..."+i);
        }
    }

}

线程实现类:

package learn.javase.threads;

public class RunnableThreadDemo implements Runnable{

    public void run() {
        for(int i=0;i<5;i++) {
            System.out.println("run..."+i);
        }
    }
}

二、线程池

1、线程池使用
package learn.javase.threads;

import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
 * 线程池
 * @author Jole
 *
 */
public class ThreadPoolDemo {

    public static void main(String[] args) {
        //使用线程池工场Executors,创建线程池
        ExecutorService es = Executors.newFixedThreadPool(2);

        //使用线程池,构造器为实现Runnable的接口,使用完后自动放回线程池
        es.submit(new ThreadPoolThreas());
        es.submit(new ThreadPoolThreas());

        //如果线程池里面线程不够了,只有等待其它线程执行完后,在使用
        es.submit(new ThreadPoolThreas());

        //一般不用关闭线程池,特殊情况关闭线程池,可以使用
//        es.shutdown();
    }

}

线程池线程实现Runnable接口:

package learn.javase.threads;

public class ThreadPoolThreas implements Runnable{

    public void run() {
        System.out.println(Thread.currentThread().getName()+"线程池的使用");
    }
}
2、线程池-实现多线程
package learn.javase.threads;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
/**
 * 通过线程池,实现Callable接口,实现多线程,并获取任务(run)返回值,和异常
 * @author Jole
 *
 */
public class ThreadPoolDemo2 {

    public static void main(String[] args) throws Exception{
        //使用线程池工场Executors创建线程池工场
        ExecutorService es = Executors.newFixedThreadPool(2);

        //通过实现Callable实现线程,提交并返回Future对象
        Future t = es.submit(new ThreadCallableDemo());
        //通过get获取返回值
        System.out.println(t.get());
    }

}

实现类Callable接口类:

package learn.javase.threads;

import java.util.concurrent.Callable;
/**
 * 
 * @author Jole
 * 使用继承Callable<T>泛型接口,实现多线程,并使用线程池
 */
public class ThreadCallableDemo implements Callable<String>{

    public String call() {
        return "实现Callable接口";
    }
}
3、实例

实例1

package learn.javase.threads;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

/**
 * 使用线程池实现异步提交求和:1+...100=   1+... 200=
 * 注意:
 *    有返回值
 *    计算不同的值
 * @author Jole
 *
 */
public class ThreadMainDemo1 {

    public static void main(String[] args) throws Exception{
        //使用线程池工场Executors创建线程池
        ExecutorService es = Executors.newFixedThreadPool(3);
        //使用线程池中的线程,并使用构造方法传参,求和并返回
        Future<Integer> f =es.submit(new CallableDemo(100));

        Future<Integer> f2 =es.submit(new CallableDemo(2100));

        System.out.println("1+...100="+f.get());
        System.out.println("1+...200="+f2.get());

        //关闭线程池
        es.shutdown();
    }

}

实现类:

package learn.javase.threads;

import java.util.concurrent.Callable;
/**
 * 通过实现Callable接口,及构造方法传参,并通过call返回值
 * @author Jole
 *
 */
public class CallableDemo implements Callable<Integer>{

    private int number;

    //创建构造方法,为了传参
    public CallableDemo(int number) {
        this.number = number;
    }

    public Integer call() {
        int sum = 0;
        //使用构造方法传的参数求和并返回
        for(int i=0;i<=number;i++) {
            sum=sum+i;
        }
        return sum;
    }
}

实例2:

package learn.javase.threads;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
/**
 * 通过线程池,实现Callable接口,实现多线程,并获取任务(run)返回值,和异常
 * @author Jole
 *
 */
public class ThreadPoolDemo2 {

    public static void main(String[] args) throws Exception{
        //使用线程池工场Executors创建线程池工场
        ExecutorService es = Executors.newFixedThreadPool(2);

        //通过实现Callable实现线程,提交并返回Future对象
        Future t = es.submit(new ThreadCallableDemo());
        //通过get获取返回值
        System.out.println(t.get());
    }

}

实现类:

package learn.javase.threads;

import java.util.concurrent.Callable;
/**
 * 
 * @author Jole
 * 使用继承Callable<T>泛型接口,实现多线程,并使用线程池
 */
public class ThreadCallableDemo implements Callable<String>{

    public String call() {
        return "实现Callable接口";
    }
}

三、线程安全

1、线程安全
package learn.javase.threads;
/**
 * 可能出现安全问题的情况:多线程,共享一个数据
 * 线程安全问题,及解决办法加synchronized,实例:卖票
 * @author Jole
 * synchronized解决线程安全问题,使用公式:
 * synchronized(锁(任意对象)){
 *       代码块
 * }
 * 
 * 注意:因为加了锁,所以效率会降低,但是安全性得到了保证,效率低的原因如下:
 * 1、每次线程都会先判断是否有锁 
 * 2、获取锁
 * 3、执行完代码块
 * 4、执行完后,还会锁
 * 5、在等下一个锁执行相同操作,相当于只能排队上一个厕所。
 */
public class ThreadSeaerfDemo {

    public static void main(String[] args) {

        //多线程出现的安全问题,票有负数还在卖
//        Tickets00 t = new Tickets00();
//        Thread t0 = new Thread(t);
//        Thread t1 = new Thread(t);
//        Thread t2 = new Thread(t);
//        t0.start();t1.start();t2.start();

        //原始代码
//        Tickets01 t = new Tickets01();
//        Thread t0 = new Thread(t);
//        Thread t1 = new Thread(t);
//        Thread t2 = new Thread(t);
//        t0.start();t1.start();t2.start();

        //第一次优化代码:同步方法,同步锁对象为:this
//        Tickets02 t = new Tickets02();
//        Thread t0 = new Thread(t);
//        Thread t1 = new Thread(t);
//        Thread t2 = new Thread(t);
//        t0.start();t1.start();t2.start();


        //第一次优化代码:同步静态方法,同步锁对象为:类名.class
        Tickets03 t = new Tickets03();
        Thread t0 = new Thread(t);
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);
        t0.start();t1.start();t2.start();


    }

}

多线线程出现的问题

package learn.javase.threads;
/** 〇、原始多线程,会出现安全问题,会出现票卖完了还在卖,也就是会出现票为负数的情况
 * @author Jole
 *
 */
public class Tickets00 implements Runnable{
    //票的数量
    private int ticket = 100;
    public void run() {
        while(true) {
            if(ticket>0) {
                //为了展示出现线程安全问题,此处让线程修改20毫秒
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //每次票减1
                System.out.println(Thread.currentThread().getName()+ " " + ticket--);
            }
        }
    }
}
2、线程安全解决方案

解决多线程问题方案使用同步锁,1、使用synchronized 2、使用Lock接口

同步锁实现原理图: 同步锁原理.png

1、synchronized

使用synchronized解决线程安全问题,使用公式:

 synchronized解决线程安全问题,使用公式:
 synchronized(锁(任意对象)){
       代码块
 }

1.1、synchronized加在外面:同步锁对象为obj

package learn.javase.threads;
/** 一、原始线程同步解决,同步锁为obj
 * 
 * synchronized解决线程安全问题,使用公式:
 * synchronized(锁(任意对象)){
 *       代码块
 * }
 * @author Jole
 *
 */
public class Tickets01 implements Runnable{
    //票的数量
    private int ticket = 100;
    Object obj = new Object();
    public void run() {
        while(true) {
            //为了解决安全问题加入了synchronized关键字
            synchronized (obj) {
                if(ticket>0) {
                    //为了展示出现线程安全问题,此处让线程修改20毫秒
                    try {
                        Thread.sleep(20);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //每次票减1
                    System.out.println(Thread.currentThread().getName()+ " " + ticket--);
                }
            }
        }
    }
}

1.2、synchronized加在非静态方法上:此时同步锁对象为this

package learn.javase.threads;
/**
 * 二、优化原始的线程安全解决办法,同步锁方法,同步锁对象为this
 * @author Jole
 *
 */
public class Tickets02 implements Runnable{
    //总票数
    private int tickets =100;
    //原始同步锁
//    Object obj = new Object();

    public void run() {
        bay();
    }

    //第一次优化,将锁和代码块抽为一个方法,测试同步锁为obj
//    public void bay() {
//        while(true) {
//            synchronized (obj) {
//                if(tickets > 0) {
//                    try {
//                        Thread.sleep(20);
//                    }catch(Exception e) {
//                        e.printStackTrace();
//                    }
//                    System.out.println(Thread.currentThread().getName()+" " +tickets-- );
//                }
//            }
//        }
//    }

    //第二次进一步优化为,同步方法,此时同步锁为:this
    //测试就可以省去以前的obj对象的创建了,代码以前更占用少一点内存了
    public synchronized void bay() {
        while(true) {
            if(tickets > 0) {
                try {
                    Thread.sleep(20);
                }catch(Exception e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+" " + tickets--);
            }
        }
    }


}

1.3、synchronized加在静态方法上:此时同步锁对象为:类名.class

package learn.javase.threads;
/**
 * 静态同步方法,此时同步锁为:类名.class
 * 如此类中的同步锁对象是:Tickets03.class
 * @author Jole
 *
 */
public class Tickets03 implements Runnable{

    //总票数
    private static int tickets =100;
    public void run() {
        buy();
    }

    //静态方法同步锁,此时代码的锁为:Tickets03.class
    public static synchronized void buy() {
        while(true) {
            if(tickets > 0) {
                try {
                    Thread.sleep(20);
                }catch(Exception e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+" " + tickets--);
            }
        }
    }
}
2、Lock

使用synchronized时,获取锁,释放锁都是JVM自动完成的,而通过Lock可以手动获取锁,释放锁。

package learn.javase.threads;
/**
 * 手动获取锁,释放锁
 * @author Jole
 *
 */
public class ThreadShouDong {

    public static void main(String[] args) {

        ThreadRunnableDemo01 t = new ThreadRunnableDemo01();
        Thread t0 = new Thread(t);
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);

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

}

Lock接口实现类:

package learn.javase.threads;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 手动获取锁,释放锁
 * @author Jole
 *
 */
public class ThreadRunnableDemo01 implements Runnable{

    //票总数
    private int tickets = 100;

    //锁 Lock接口的实现类ReentrantLock
    private Lock lock = new ReentrantLock();

    public void run() {
        while(true) {
            //获取锁
            lock.lock();
            try {
                if(tickets > 0) {
                    Thread.sleep(20);
                    System.out.println(Thread.currentThread().getName()+" "+tickets--);
                }
            }catch(Exception e) {
                e.printStackTrace();
            }finally {
                //释放锁
                lock.unlock();
            }
        }
    }
}
3、注意点

使用多线程为了保证数据的安全性,加了synchronized或Lock保持同步,因此效率上就降低了。

可能出现安全问题的情况:多线程,共享一个数据。

使用synchronized时,同步锁必须保证为同一个同步锁对象才行。

四、死锁

线程死锁.png 死锁:多个线程,对方互相等待获取对方的锁。双方一直处于等待获取对方锁对象状态,也就是死锁。

package learn.javase.threads;
/**
 * 死锁:多个线程,对方互相等待获取对方的锁。双方一直处于等待获取对方锁对象状态,也就是死锁。
 * @author Jole
 *
 */
public class DealThreadMainDemo {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        DetalThread death = new DetalThread();
        Thread t0 = new Thread(death);
        Thread t1 = new Thread(death);
        t0.start();
        t1.start();
    }

}

同步锁对象A:

package learn.javase.threads;
/**
 * 同步锁A,对象
 * @author Jole
 *
 */
public class LockA {
    private LockA() {

    };

    public static final LockA lockA = new LockA();
}

同步锁对象B:

package learn.javase.threads;
/**
 * 同步锁B,对象
 * @author Jole
 *
 */
public class LockB {

    public LockB() {

    }

    public static final LockB lockB = new LockB();
}

模拟出现死锁情况,实现类:

package learn.javase.threads;

public class DetalThread implements Runnable{

    private int number = 0;
    public void run() {
        while(true) {
            if(number % 2 ==0) {
                synchronized (LockA.lockA) {
                    System.out.println(Thread.currentThread().getName()+"线程,"+"第"+number+"次,"+"if抢到资源--获取到--同步锁A对象");
                    synchronized (LockB.lockB) {
                        System.out.println(Thread.currentThread().getName()+"线程,"+"第"+number+"次,"+"if抢到资源--获取到--同步锁B对象");
                    }
                }
            }else {
                synchronized (LockB.lockB) {
                    System.out.println(Thread.currentThread().getName()+"线程,"+"第"+number+"次,"+"else抢到资源--获取到--同步锁B对象");
                    synchronized (LockA.lockA) {
                        System.out.println(Thread.currentThread().getName()+"线程,"+"第"+number+"次,"+"else抢到资源--获取到--同步锁A对象");
                    }
                }
            }
            number++;
        }
    }
}

注意获取的锁是那个。

五、线程等待、唤醒

多线程的等待与唤醒.png

实例:input拷贝数据对象,output输入对象:如果不是用线程等待与唤醒,出现问题:拷贝的数据太快,还来不及输出,可能出现输出的数据错乱。使用线程等待和唤醒,可以控制拷贝一个后,拷贝线程等待,然后输出线程输出,输出后,输出线程等待,然后拷贝线程唤醒,进行拷贝数据,依次循环。从而保证拷贝一个数据,输出一个数据。

package learn.javase.threads;
/**
 * 线程等待、唤醒:
 * 涉及同步锁是否是同一个锁,只有锁调用wait和notify才有效,不然抛出异常。
 * 正常情况是交替出现,不会出现数据错乱情况。
 * @author Jole
 * 本类是主测试类
 * 该实例:input拷贝数据对象,output输入对象:
 * input可能出现拷贝对象拷贝很快,output输出对象很慢,导致output输出数据错乱,因此只能单个拷贝输出,且每次拷贝完后需wait等待,然后等输出完成后在拷贝。
 * 因此需要拷贝完成后,等待,并唤醒输出,等输出完成前唤醒拷贝,然后自己在等待,然后拷贝又拷贝,然后又唤醒输出,如此循环。
 * 实现方法加一个flag标记,是该拷贝还是输出
 */
public class WatiNotifyThreadMainDemo {

    public static void main(String[] args) {
        //操作的是同一个对象锁,不然拷贝和输出都是用自己的锁this,也会发生数据错乱
        ResourceData resource = new ResourceData();

        //保证是同一个锁对象,所以都写了构造方法传入同一个锁对象
        InputDemo input = new InputDemo(resource);
        OutputDemo output = new OutputDemo(resource);

        Thread t0 = new Thread(input);
        Thread t1 = new Thread(output);

        t0.start();
        t1.start();
    }

}

要拷贝的数据类:

package learn.javase.threads;
/**
 * 操作数据
 * @author Jole
 *
 */
public class ResourceData {

    public String name;
    public String sex;

    public  boolean flag;
}

拷贝线程:

package learn.javase.threads;
/**
 * 拷贝线程:
 * 当标记flag=true时,拷贝完成,需等待
 * @author Jole
 *
 */
public class InputDemo implements Runnable {
    private ResourceData resourceData;
    public InputDemo(ResourceData resourceData) {
        this.resourceData = resourceData;
    }

    @Override
    public void run() {
        int  number=0;
        while(true) {
            synchronized (resourceData) {
                //如果为ture拷贝完成,需等待
                if(resourceData.flag) {
                    try {
                        resourceData.wait();
                    }catch(Exception e) {
                        e.printStackTrace();
                    }
                }
                //拷贝值,比如写死拷贝张三,30岁,lishi,20岁
                if(number % 2==0) {
                    resourceData.name = "张三";
                    resourceData.sex = "男";
                }else {
                    resourceData.name = "Lishi";
                    resourceData.sex = "nv";
                }

                //设置true,以便下次进入等待wait
                resourceData.flag = true;

                //唤醒输出线程
                resourceData.notify();
            }

            number++;
        }

    }

}

输出线程:

package learn.javase.threads;
/**
 * 输出线程:
 * 当标记flag=false时,输出完成,需等待
 * @author Jole
 *
 */
public class OutputDemo implements Runnable {

    private ResourceData resourceData;
    public OutputDemo(ResourceData resourceData) {
        this.resourceData = resourceData;
    }

    @Override
    public void run() {
        while(true) {
            synchronized (resourceData) {
                //如果为false,则输出完成,等待
                if(!resourceData.flag) {
                    try {
                        resourceData.wait();
                    }catch(Exception e) {
                        e.printStackTrace();
                    }
                }
                //否则输出,并唤醒拷贝
                System.out.println(resourceData.name+" .. "+ resourceData.sex);
                resourceData.flag = false;
                resourceData.notify();
            }
        }
    }

}

本文链接:https://www.yanxizhu.com/post/thread.html

-- EOF --

Comments