Java 多线程基础

标签(空格分隔): java


线程和进程

进程具有独立的数据空间,是系统进行资源分配和调度的独立单位

独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间,在没有进 过进程本身允许的情况下,一个用户进程不可以访问其他用户的地址空间。
动态性:程序只是一个静态的指令集合,而进程是正在操作系统中活着的指令集合。在进程中加入了时间的概念,进程具有自己的生命周期和个不同的状态。这些概念在程序中都是不具备的。
并发性:多个进程可以在多个处理器上并发执行,多个进程之间不会相互影响。

线程:是轻量级的进程,是进程的执行单元,它拥有自己的堆栈,自己的程序计数器和自己的局部变量,但不拥有系统资源,因为多个线程共享父进程的全部资源。

一个线程可以创建和撤销另一个线程,多个线程可以在一个进程中并发执行。

线程的创建和启动

1、继承Thread类创建线程

1.继承Thread类,重写run()方法。

public class MyThread extends Thread {

    @Override
    public void run() {
    }
}

2.实例化对象,调用对象的start()方法

 MyThread thread = new MyThread();
 thread.start();

2、使用静态代理模式:实现Runnable接口

1.实现Runnable接口

public class MyRun implements Runnable {
    @Override
    public void run() {
        //do Something
    }
}

2.new Thread对象,传入Runnable接口实例,调用Thread类的start方法

 Thread thread = new Thread(new MyRun());
 thread.start();

两种方式都可以创建线程,但是第二种方式,实现起来更加灵活,同时也可以实现多线程数据的共享,推荐使用第二种方式创建线程。

3、实现Callable接口创建线程

实现Callable接口的类和实现Runnable接口的类都是可以被线程执行的任务。
几点不同:

  1. Callable规定的方法是call()方法,而Runnable规定的方法是run()
  1. call()方法可以抛出异常,run()不能抛出异常
  2. Callable执行任务后可以拿到返回值,运行Callable任务后可以拿到一个Future对象,Runnable的任务是不能带有返回值的,Future表示任务异步执行的结果,他提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果,通过Futrue对象,可以了解任务执行的情况,可以取消任务的执行,还可以获取任务的执行结果。

缺点是:比较繁琐

实现步骤:

  1. 创建Callable实现类重写call()方法
  2. 借助执行调度服务ExecutorService获取Future对象
    ExecutorService ser = Executors.newFixedThreadPool(2);
    Future result = ser.submit(实现类对象);
  3. 获取值 result.get();
  4. 停止服务ser.shutdownNow();

第二种:实现方式
由于Callable接口不是Runnable接口的子类对象,因此不能直接作为Thread类的Target对象,同时Callable接口里的call()方法拥有返回值,这个又该如何接受呢?
java为我们提供了一个Future接口,这个接口是为了用于接受Callable里的返回结果,同时为我们提供了一个是实现类FutureTask,这个类同时实现了Future接口和Runnable接口,这个类的实例需要一个Callable对象的实例,因此我们可以将Callable包装成FutureTask的实例,这样就同时解决了上面的两个问题。

    private static void test() {
        MyCallable call = new MyCallable();
        FutureTask task = new FutureTask<>(call);
        Thread thread = new Thread(task);
        try {
            System.out.println("主线程运行");
            thread.start();
            int result = task.get();
            System.out.println("\n主线程阻塞完成");
            System.out.println(result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println("程序退出");
    }
}

class MyCallable implements Callable {

    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 0; i < 10; i++) {
            Thread.sleep(1000);
            System.out.print(i + "\t");
            sum += i;
        }
        return sum;
    }
}   

Thread类的常用方法

构造方法

 /**
   这个是最全的构造方法
   根据里面的参数可以组合出多种构造
   stackSize是线程运行时的堆栈大小
 */
Thread(ThreadGroup group, Runnable target, String name, long stackSize) 

静态方法
    Thread.sleep();
    Thread.currentThread()

线程状态(5种状态)

一、新生状态:线程new出来时候的状态。拥有自己的堆栈空间。

二、就绪状态:新生状态---->调用start()方法--->就绪状态。线程在执行start()方法后并不是一定处于运行的状态,只是可以接受CPU调度,叫可运行状态----就绪状态,一旦获取了CPU的执行权,线程就进入运行状态,并自动调用自己的run()

三、运行状态:就绪状态的线程,在接受了CPU调度之后,开始运行,在运行状态的线程执行自己run()方法中的代码,除非调用其他方法而终止,或等待某资源而阻塞,有或运行完成任务而死亡。如果在给定的时间片内没有执行结束,就会被系统换下来回到等待执行状态(这个过程我们称为cpu的任务切换---并发执行)。

四、终止状态(死亡状态):当线程体执行完毕,则线程进入终止状态,线程结束。终止状态是线程生命周期中的最后一个阶段。线程结束的原因有两个,一个是运行的线程完成了它的全部工作,另一个是线程被强制性的终止,如执行stop()方法和destroy()方法来终止线程(不推荐使用这两个方法,前者会产生异常,后者是强制终止,不会释放锁)。

五、阻塞状态:当线程在运行时,有阻塞的事件发生,则线程暂停运行,进入阻塞状态,进入阻塞状态的线程,无法没有CPU执行权,无法接受CPU的任务调度。当阻塞解除时,该线程将再次进入就绪状态,重新拥有CPU执行权,可以接受CPU调度。处于正在运行状态的线程,在某些情况下,执行了sleep()方法,或等待IO设备等资源,或让出CPU的执行权并暂时停止运行,进入阻塞状态,在阻塞状态的线程就不能就如就绪队列,只有当引起阻塞的原因解除时,如睡眠时间到,或等待的IO设备空闲下来,线程便进入就绪状态,重新到队列中排队等待,被系统选中后从原来停止的位置开始继续运行。

停止线程的方法

停止一个线程很简单,就是让这个线程体执行完成就行了,因此我们使用一个flag来判断是否结束以及何时结束这个循环,就行了。
一般的情况下:我们自己写一个方法,调用这个方法,则设置标记位,结束运行条件,促使线程体执行结束。

 class MyCallable implements Callable {

public boolean flag = true;

@Override
public Integer call() throws Exception {
    int i = 0;
    for (; i < 20; i++) {
         if (!flag)
            break;
            Thread.sleep(1000);
            System.out.print(i + "\t");
    }
    return i;
}

/**
*为外界提供停止线程的控制方法
*/
  public void stop(){
     flag = false;
   }
 }

多线程并发安全问题

由于线程在系统的调度中具有随机性,因此当多个线程在访问同一份资源时,很容易出现错误.

一些很经典的问题:卖票和取钱,
下面我们根据一个经典的卖票程序来分析一下和解决一下线程安全性问题

    private int ticket = 20;
    @Override
    public void run() {
        while (ticket>0) {
            ticket--;
            System.out.println(Thread.currentThread().getName() + "=====" +ticket );
           }
        }

当上面的程序放到多线程中使用时,就会出现并发安全问题,可能出现的现象是:同一份资源出现重复调用,
例如,当一个线程当ticket--;代码完成后现成的cpu资源被抢夺,这个时候,第二个线程也执行了ticket--;这个时候
出现数字丢失,有的字重复出现,同时t--;在程序中也是分两步执行的。有可能执行了减法操作,还没来得及赋值,就被其它线程抢夺走了CPU执行权,使得第一个减法操作无效丢失。

解决办法:使用Synchronized同步--->同步代码块和同步函数

public class SaleTicket implements Runnable {

    private int ticket = 10;
    
    @Override
    public void run() {
        synchronized (SaleTicket.class) {
            while (ticket > 0) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ticket--;
                System.out.println(Thread.currentThread().getName() + "=====" + ticket);
            }
        }
    }

注:
如果代码加了同步还是出现了安全问题,那么就看:

1.多个线程是不是操作的是同一份资源
2.同步的代码地方是不是使用的是同一个锁
3.是不是所有的共享数据资源都加入了同步

同步方法持有的锁,是该方法所属的对象,静态同步代码块持有的锁,是该类的字节码文件。

单例设计模式---懒汉式

class Single{
  private static Single instance = null;
  private Single(){}

  public static Single getIntance(){
    if(instance == null){
        synchronized(Single.class){
          if(instance == null) {
              instance = new Single();
          }
       }
       return instance;
    }
}

懒汉式的单例模式的优点是延时加载,但是在多线程中会出现问题,使用同步代码块可以解决该问题,使用双重判断可以稍微提高一下效率。

你可能感兴趣的:(Java 多线程基础)