java多线程

一步一步基础来

进程

  • 正在运行的程序。例如:QQ,酷狗音乐
  • 进程是系统进行资源分配和调用的独立单位。每一个进程都有它自己的内存空间和系统资源。

多进程

  • 单进程的计算机只能做一件事情。然而现在的电脑手机是多进程的,多进程允许计算机同时间做许多事情
  • 可以在一个时间段内执行多个任务。
  • 可以提高CPU的使用率。
  • 每个程序其实并不是同时间运行的,只是CPU在多个进程之间的高效,高速切换让我们觉得这么多程序是同一时刻在进行。

线程

  • 是程序的执行单元,执行路径。是程序使用CPU的最基本单位
  • 同一个进程内又可以执行多个任务,而这每一个任务就是一个线程。例如:使用QQ时候发消息,发图片,发语音等等。
  • 单线程:如果程序只有一条执行路径。代码上怎么体现,就是我们一条main方法执行到,然后结束
  • 多线程:如果程序有多条执行路径。多线程就是在执行main方法的同时,去开辟另外一个任务,和main方法抢夺CPU的时间片,执行。

CPU时间片


时间片即CPU分配给各个程序的时间,每个线程被分配一个时间段,称作它的时间片,即该进程允许运行的时间,使各个程序从表面上看是同时进行的。如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。而不会造成CPU资源浪费。在宏观上:我们可以同时打开多个应用程序,每个程序并行不悖,同时运行。但在微观上:由于只有一个CPU,一次只能处理程序要求的一部分,如何处理公平,一种方法就是引入时间片,每个程序轮流执行。


多线程

  • 多线程的存在,不是提高程序的执行速度。其实是为了提高应用程序的使用率。
  • 程序的执行其实都是在抢CPU的资源,CPU的执行权。
  • 多个进程是在抢这个资源,而其中的某一个进程如果执行路径比较多,就会有更高的几率抢到CPU的执行权。
  • 不能保证哪一个线程能够在哪个时刻抢到,所以线程的执行有随机性。

并发和并行

  • 并行:指在某一个时间内同时运行多个程序。
  • 并发:指在某一个时间点同时运行多个程序。
注意:进程是拥有资源的基本单位,线程是CPU调度的基本单位

Java实现多线程

  • java不能直接调用系统功能,但是C/C++ 语言可以。所以java通过调用C/C++ 已经写好 的程序去实现多线程,java将C/C++的程序包装成一个类 (Thread),我们通过调用此类实现多线程。
  • 创建多线程的方法
    • 第一种方法是将类声明为 Thread 的子类,即就是继承Thread。该子类应重写 Thread 类的 run 方法。
      接下来可以分配并启动该子类的实例:
      • 1.写一个类继承Thread类
      • 2.重写Thread类中的run()方法
      • 3.创建一个该类的实例。
      • 4.开启线程 :使用start() 方法开启线程。
    • 第二种方法是声明实现 Runnable 接口的类。
      • 1.定义一个类实现Runnable接口
      • 2.重写接口中的run()方法。Thread(Runnable target) 分配新的 Thread 对象。
      • 3.创建线程对象,把Runnable接口的子类对象,作为参数传递进来
      • 4.开启线程
    • 第三种通过Callable和FutureTask创建线程
      • 1.创建Callable接口的实现类,并实现Call方法
      • 2.创建Callable实现类的实现,使用FutureTask类包装Callable对象,该FutureTask对象封装了Callable对象的Call方法的返回值
      • 3.使用FutureTask对象作为Thread对象的target创建并启动线程
      • 4.调用FutureTask对象的get()来获取子线程执行结束的返回值
    • 第四种通过线程池创建线程

前面两种可以归结为一类:无返回值,原因很简单,通过重写run方法,run方式的返回值是void,所以没有办法返回结果
后面两种可以归结成一类:有返回值,通过Callable接口,就要实现call方法,这个方法的返回值是Object,所以返回的结果可以放在Object对象中


实现多线程方法一:

public class MyThread extends Thread {
    //run() 是线程要执行的方法,方法里面的代码是让线程来执行的
    @Override
    public void run() {
        //就是需要线程来执行的代码
        //一般来说,一些耗时的操作,需要我们开启一个线程,在run()方法里面去操作
        //复制操作
        //模拟耗时操作
        for (int i = 0; i < 100; i++) {
            System.out.println(i);

        }
    }
}
public class ThreadTest {
    public static void main(String[] args) {
        MyThread th = new MyThread();
        th.run(); 
        th.start();//开启线程
//      th.start();重复开启线程会抛异常
//      IllegalThreadStateException异常:重复开启时候抛出此异常。
//      可以再开启一个
        MyThread th2 = new MyThread();
        th2.start();
    }
}

实现多线程方法二:

public class MyRunable implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            //获取当前正在执行的线程对象
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            String name = Thread.currentThread().getName();
            System.out.println(name + "==" + i);
        }
    }

}
实现Runnable接口 这种方式扩展性强
实现一个接口 还可以再去继承其他类
可以避免由于Java单继承带来的局限性。


实现多线程方法三:

public class Tickets<Object> implements Callable<Object>{

    //重写call方法
    @Override
    public Object call() throws Exception {
        // TODO Auto-generated method stub
        System.out.println(Thread.currentThread().getName()+"-->我是通过实现Callable接口通过FutureTask包装器来实现的线程");
        return null;
    }   
}
public class CallableTest{
    public static void main(String[] args) {
        // TODO Auto-generated method stub

        Callable<Object> oneCallable = new Tickets<Object>();
        FutureTask<Object> oneTask = new FutureTask<Object>(oneCallable);

        Thread t = new Thread(oneTask);

        System.out.println(Thread.currentThread().getName());

        t.start();

    }

}

实现多线程方法四:通过线程池创建线程



线程安全问题

  • 线程不安全:即就是多线程的操作影响最终数据安全的问题。
  • 产生数据安全问题的必要条件:
    • 1.多线程环境
    • 2.要有多个线程共享数据
    • 3.有多条语句操作共享数据
  • 同步性:数据结果的不正确,这是因为多线程之间的同步性,比如:当线程1修改了共享数据的值,另一个线程此时要拿值运算,时间上的不同步,导致拿运算的值是旧值,这就导致了最终结果的影响 。
  • 所以,java设计了锁和同步代码块的概念。
锁和同步代码块
  • 在java编程中,经常需要用到同步,而用得最多的也许是synchronized关键字了,因为synchronized关键字涉及到锁的概念,所以先来了解一些相关的锁知识。java的内置锁:每个java对象都可以用做一个实现同步的锁,这些锁成为内置锁。线程进入同步代码块或方法的时候会自动获得该锁,在退出同步代码块或方法时会释放该锁。获得内置锁的唯一途径就是进入这个锁的保护的同步代码块或方法。java内置锁是一个互斥锁,这就是意味着最多只有一个线程能够获得该锁,当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞, 直到线程B释放这个锁,如果B线程不释放这个锁,那么A线程将永远等待下去。
synchronized关键字
  • 这个关键字就是同步的意思,用来构成同步代码块,在这个关键字的小括号里,我们需要放置一把锁。这里锁也分了两种种。分别是对象锁和类锁。
  • java的对象锁和类锁在锁的概念上基本上和内置锁是一致的,但是,两个锁实际是有很大的区别的,对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的class对象上的。我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的.
  • 多个线程要共享一个锁对象,使用同步代码块的弊端,耗费资源,效率不高
synchronized (锁对象){ 同步代码块的锁对象属于互斥锁
            //需要同步的代码

        }
  • 那么锁对象应该是什么?
    • 同代码代码块的锁对象:是任意对象
    • 同步方法的锁对象是this
    • 静态同步方法的锁对象是当前类的字节码对象。
  • 锁是控制多个线程对共享资源进行访问的工具。
  • 通常,锁提供了对共享资源的独占访问。
  • 一次只能有一个线程获得锁,对共享资源的所有访问都需要首先获得锁。
因此有了同步代码块,我们可以控制多个线程对共享资源的访问,同时也建立起了线程之间的同步性。使用同步代码块的弊端,耗费资源,效率不高
JDK5以后提供了一个新的锁对象Lock,用法和同步代码块一样,解决线程安全问题。
  • void lock():上锁
  • void unlock():解锁

Lock.lock()
.......
.......需要同步的代码
.......
Lock.unlock()

线程池

  • 一个容器,装有线程对象的容器,可以帮我们管理线程对象
  • 线程池可以预先,帮我们创建一些数量的线程对象,放在池子中,等有任务需要执行了,我就可以让线程中的线程去执行任务,执行完了,可以回收。不用手动的去创建线程了。
  • 获取线程池:
    • 在JDK1.5之前线程池需要自己手动实现
    • JDK1.5之后Java给我们内置好了线程池
    • 通过一个工厂类立马的静态方法来获取线程池对象
    • Executors工厂类来产生线程池
    • 构造方法:
      • public static ExecutorService newCachedThreadPool ():根据任务的数量来创建线程对应的线程个数
      • public static ExecutorService newFixedThreadPool ( int nThreads):固定初始化几个线程
      • public static ExecutorService newSingleThreadExecutor ():初始化一个线程的线程池
    • 步骤:
      • 构造方法获取线程池对象
      • 往线程池提交任务
      • 提交一个返回值的任务用于执行,返回一个表示任务的未决结果的 Future。
      • 提交一个Runnable任务用于执行,并返回一个表示该任务的 Future。
      • 关闭线程池
  • 如果Executor后台线程池还没有完成Callable的计算,这调用返回Future对象的get()方法,会阻塞直到计算完成。
        //获取线程池对象(三个构造方法)
        ExecutorService service = Executors.newFixedThreadPool(3);
        //往线程池里面提交任务(可以提交有返回值的用future接受,get方法获取返回结果,也可以提交无返回值的用future接受)
        Future<Integer> submit = service.submit(new MyCallable(10));
        Future<Integer> submit1 = service.submit(new MyCallable(100));
        Future<Integer> submit2 = service.submit(new MyCallable(1000));
        //获取Callable任务执行完之后的返回结果 Future 里面有一个get()方法可以获取返回的结果
        System.out.println(submit.get());
        System.out.println(submit1.get());
        System.out.println(submit2.get());
        //关闭线程池
        service.shutdown();

定时器

  • 一个工具类,在指定的时间或日期执行定时任务
  • Timer一种工具,线程用其安排以后在后台线程中执行的任务。可安排任务执行一次,或者定期重复执行。
  • 使用步骤:
    • 创建一个新计时器。(构造器)
      • Timer timer = new Timer();
    • 执行定时任务。
      • public void schedule (TimerTask task,long delay)?/在指定时间后,执行定时任务
      • public void schedule (TimerTask task,long delay, long period);首次延迟delay,每次间隔period执行。
      • public void schedule (TimerTask task, Date time);接受一个指定data的时间执行任务
      • public void schedule (TimerTask task, Date firstTime,long period)
    • 取消此计时器任务。
      • boolean cancel()
public class MyTimerTask extends TimerTask {
    Timer timer;

    public MyTimerTask(Timer timer) {
        this.timer = timer;
    }

    @Override
    public void run() {
       // 定时关机
        Runtime runtime = Runtime.getRuntime();
        try {
            runtime.exec("shutdown -s -t 0");
        } catch (IOException e) {

            e.printStackTrace();
        }
        //关闭定时器
        //timer.cancel();
    }
}
public class MyTest3 {
    public static void main(String[] args) throws ParseException {
        //在指定日期来执行定时任务
        Timer timer = new Timer();
        String dateStr = "2018-11-18 10:40:00";
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date date = simpleDateFormat.parse(dateStr);
        timer.schedule(new MyTimerTask(timer), date);
    }
}

Java内存模型

  • Java内存模型规定了所有的变量都存储在主内存中。每条线程中还有自己的工作内存,线程的工作内存中保存了被该线程所使用到的变量这些变量是从主内存中拷贝而来。线程对变量的所有操作(读取,赋值)都必须工作内存中进行。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。

  • 举个例子:两个线程AB分别共享和拥有修改主存中一个变量。A线程和B线程会分别在主存中拷贝一份变量,在自己的内存空间中修改,对于线程AB来说,他们俩之间的工作情况不能直接相互看见,需要通过主存才能看见。这就是内存可见性。

volatile关键字:保证可见性。
  • 当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
  • 而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的。 当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
  • 另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
  • volatile
    • 1.不具有互斥性
    • 2.不保证原子性
  • volatile关键字:当多个线程进行操作共享数据时,可以保证内存中的数据可见。相较于synchronized 是一种较为轻量级的同步策略
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值