java多线程

1.    线程的实现

a.    继承Thread类:在java.lang包中定义,继承Thread类必须重写run()方法

class MyThread extends Thread{

   private static int num = 0;

 

   public MyThread(){

       num++;

    }

 

   @Override

   public void run() {

       System.out.println("主动创建的第"+num+"个线程");

    }

}

publicclass Test {

    public static void main(String[] args)  {

        System.out.println("主线程ID:"+Thread.currentThread().getId());

        MyThread thread1 = newMyThread("thread1");

        thread1.start();

        MyThread thread2 = new MyThread("thread2");

        thread2.run();

    }

}

 

classMyThread extends Thread{

    private String name;

 

    public MyThread(String name){

        this.name = name;

    }

 

    @Override

    public void run() {

       System.out.println("name:"+name+" 子线程ID:"+Thread.currentThread().getId());

    }

}

创建好自己的线程类后,就可以创建线程对象了,然后通过start()方法去启动线程。注意,不是通过run()方法启动线程,run方法只是定义需要执行的任务,如果调用run方法,即相当于在主线程中执行run方法,跟普通的方法调用没有任何区别,此时并不会创建一个新的线程来执行定义的任务。

b.    实现Runnable接口:通过实现Runnable接口来实现类似的功能。实现Runnable接口必须重写run方法。

public class Test {

   public static void main(String[] args) {

       System.out.println("主线程ID:"+Thread.currentThread().getId());

       MyRunnable runnable = new MyRunnable();

       Thread thread = new Thread(runnable);

       thread.start();

    }

}

class MyRunnable implements Runnable{

   public MyRunnable() {

    }

 

   @Override

   public void run() {

       System.out.println("子线程ID:"+Thread.currentThread().getId());

    }

}

public class Test {

   public static void main(String[] args) {

        System.out.println("主线程ID:"+Thread.currentThread().getId());

       MyRunnable runnable = new MyRunnable();

        Thread thread = new Thread(runnable);

        thread.start();

    }

}

class MyRunnable implements Runnable{

   public MyRunnable() {

    }

 

   @Override

   public void run() {

       System.out.println("子线程ID:"+Thread.currentThread().getId());

    }

}

这种方式必须将Runnable作为Thread类的参数,然后通过Thread的start方法来创建一个新线程来执行该子任务。如果调用Runnable的run方法的话,是不会创建新线程的,这根普通的方法调用没有任何区别。

在Java中,这两种方式都可以用来创建线程去执行子任务,直接继承Thread类,可能比实现Runnable接口看起来更加简洁,但是由于Java只允许单继承,所以如果自定义需要继承其他类,则只能选择实现Runnable接口。

c.    使用ExecutorService、Callable、Future实现有返回结果的多线程

ExecutorService、Callable、Future这个对象都属于Executor框架中的功能类。返回值的任务必须实现Callable接口,无返回值的任务必须实现Runable接口。执行Callable任务后可以获取一个Future对象,在该对象上调用get就可以获取到Callable任务返回的Object,在结合线程池接口ExecutorService就可以实现有返回值结果的多线程了。

/**

* 有返回值的线程

*/

@SuppressWarnings("unchecked") 

public class Test { 

public static void main(String[] args)throws ExecutionException, 

   InterruptedException { 

  System.out.println("----程序开始运行----"); 

  Date date1 = new Date(); 

 

   inttaskSize = 5; 

   // 创建一个线程池 

  ExecutorService pool = Executors.newFixedThreadPool(taskSize); 

   // 创建多个有返回值的任务 

  List<Future> list = new ArrayList<Future>(); 

   for(int i = 0; i < taskSize; i++) { 

   Callable c = new MyCallable(i + " "); 

    //执行任务并获取Future对象 

   Future f = pool.submit(c); 

    //System.out.println(">>>" + f.get().toString()); 

   list.add(f); 

  } 

   // 关闭线程池 

   pool.shutdown(); 

 

   // 获取所有并发任务的运行结果 

   for(Future f : list) { 

    //从Future对象上获取任务的返回值,并输出到控制台 

   System.out.println(">>>" + f.get().toString()); 

  } 

 

  Date date2 = new Date(); 

  System.out.println("----程序结束运行----,程序运行时间【"

     +(date2.getTime() - date1.getTime()) + "毫秒】"); 

 

class MyCallable implementsCallable<Object> { 

private String taskNum; 

 

MyCallable(String taskNum) { 

  this.taskNum = taskNum; 

 

public Object call() throws Exception { 

  System.out.println(">>>" + taskNum + "任务启动"); 

  Date dateTmp1 = new Date(); 

  Thread.sleep(1000); 

  Date dateTmp2 = new Date(); 

  long time = dateTmp2.getTime() - dateTmp1.getTime(); 

  System.out.println(">>>" + taskNum + "任务终止"); 

  return taskNum + "任务返回运行结果,当前任务时间【" + time + "毫秒】"; 

}

}

2.    线程的状态

创建(new)状态:准备好了一个多线程的对象。

就绪(runnable)状态:调用了start()方法,等待CPU进行调度。

运行(running)状态:执行run()方法。

阻塞(blocked)状态:暂时停止执行,可能将资源交给其他线程使用。

终止(dead)状态:线程销毁

Time waiting(睡眠或等待一定的事件)、waiting(等待被唤醒)、blocked(阻塞)。

注:sleep和wait区别:

Sleep是Thread类的方法,wait是Object类中定义的方法;Thread.sleep不会导致锁行为的改变,如果当前线程是拥有锁的,那么Thread.sleep不会让线程释放锁;Thread.sleep和Object.wait都会暂停当前的线程。OS会将执行时间分配给其他线程。区别是调用wait后,调用对象的wait()方法导致当前线程放弃对象的锁,进入对象的等待池,需要别的线程执行notify/notifyAll(notify唤醒一个处于等待状态的线程,当调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关;notifyAll唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有的线程,而是让他们竞争,只有获得锁线程才能进入就绪状态)才能重新获得CPU执行时间。

sleep()方法:方法sleep()的作用是在指定的毫秒数内让当前“正在执行的线程”休眠(暂停执行)。这个“正在执行的线程”是指this.currentThread()返回的线程。sleep相当于让线程睡眠,交出CPU,让CPU去执行其他的任务。但是有一点要非常注意,sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象。

Yield()方法:调用yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程。它跟sleep方法类似,同样不会释放锁。但是yield不能控制具体的交出CPU的时间,另外,yield方法只能让拥有相同优先级的线程有获取CPU执行时间的机会。注意,调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的。

join()方法:在很多情况下,主线程创建并启动了线程,如果子线程中药进行大量耗时运算,主线程往往将早于子线程结束之前结束。这时,如果主线程想等待子线程执行完成之后再结束,比如子线程处理一个数据,主线程要取得这个数据中的值,就要用到join()方法了。方法join()的作用是等待线程对象销毁。

setDaemon和isDaemon:

用来设置线程是否成为守护线程和判断线程是否是守护线程。守护线程和用户线程的区别在于:守护线程依赖于创建它的线程,而用户线程则不依赖。举个简单的例子:如果在main线程中创建了一个守护线程,当main方法运行完毕之后,守护线程也会随着消亡。而用户线程则不会,用户线程会一直运行直到其运行完毕。在JVM中,像垃圾收集器线程就是守护线程。如果用户线程全部撤离那么守护线程就没啥线程好服务的了,所以虚拟机就退出了。

停止线程:在Java中有以下3种方法可以终止正在运行的线程:

使用退出标志,使线程正常退出,也就是当run方法完成后线程终止

使用stop方法强行终止线程,但是不推荐使用这个方法,因为stop和suspend及resume一样,都是作废过期的方法,使用他们可能产生不可预料的结果。

使用interrupt方法中断线程,但这个不会终止一个正在运行的线程,还需要加入一个判断才可以完成线程的停止。

同步与死锁:同步代码块,在代码块上加上”synchronized”关键字,则此代码块就称为同步代码块。同步代码块格式

synchronized(同步对象){

 需要同步的代码块;

}

同步方法

除了代码块可以同步,方法也是可以同步的

方法同步格式

synchronized void 方法名称(){}

3.    线程的sleep()方法和yield()方法有什么区别?

a.    sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会,yield()方法只会给相同优先级或更高优先级的线程以运行的机会。b.线程执行sleep()方法后转入阻塞状态,而执行yield()方法后转入就绪(ready)状态。c.sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常。d.sleep()方法比yield()方法(跟操作系统CPU调度相关)具有更好的可移植性。

4.    volatile关键字

volatile是轻量级的synchronized,如果一个变量使用volatile则它比使用synchronized的成本更低,因为它不会引起线程上下文的切换和调度。一个变量如果用volatile修饰了,则java可以确保所有线程看到这个变量的值是一致的,如果某个线程对volatile修饰的共享变量进行更新,那么其他线程可以马上看到这个更新。程序运行的数据是存储在主存中,读写主存中的数据没有执行CPU中执行指令的速度快,如果任何的交互都需要与主存打交道则会大大影响效率,所以就有了CPU高速缓存。CPU告诉缓存为某个CPU独有,只与在该CPU运行的线程有关。这样会导致数据一致性的问题,在程序运行中,会将运行所需要的数据复制一份到CPU高速缓存中,在进行运算时CPU不再跟主存打交道,而是直接从高速缓存读写数据,只有当运行结束后才会将数据刷新到主存中。解决缓存一致性方案有两种: a.通过在总线加LOCK锁方式(这样只能有一个CPU运行,其他CPU都得阻塞,效率较低)b.通过缓存一致性协议(确保每个缓存中使用得共享变量得副本是一致得,当某个CPU在写数据时,如果发现操作得变量是共享变量,则会通知其他CPU告知该变量得缓存是无效得,因此其他CPU在读取变量时,发现其无效会从主存加载数据)。Java语言通过volatile和synchronized两个关键字来保证线程之间操作得有序性,volatile指令“禁止指令重排序”,synchronized关键字“一个变量在同一时刻只允许一条线程对其进行lock操作”。当一个变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。

5.    synchronized的局限性和Lock的优点

synchronizedJava的关键字,是Java的内置特性,在JVM层面实现了对临界资源的同步互斥访问,但synchronized粒度有些大,在处理实际问题时存在诸多局限性。   如果一个代码块被synchronized关键字修饰,当一个线程获取了对应的锁,并执行该代码块,其他线程只能一直等待直至占有锁的线程释放锁。占有锁的线程释放锁一般是下面这三种情况之一:占有锁的线程执行完了该代码块,然后释放对锁的占有;占有锁的线程执行发生了异常,此时JVM会让线程自动释放锁;占有锁线程进入WAITING状态从而释放锁,例如在线程中调用wait()方法。为什么使用lock呢?a.在使用synchronized关键字的情形下,假如占有锁的线程由于要等待IO或者其他原因被阻塞了,但又没有释放锁那么其他线程就只能一直等待,因此,就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间 (解决方案:tryLock(long time, TimeUnitunit)) 或者能够响应中断 (解决方案:lockInterruptibly())),这种情况可以通过 Lock 解决。b.读写文件时,读跟写操作会发生冲突现象,写跟写操作也会发生冲突现象,但是读跟读操作不会发生冲突现象。但使用snchronized关键字实现同步的话,会导致多个线程进行读操作时,也只有一个可以进行读操作,其他线程只能等待锁的释放而无法进行读操作。 Lock可以解决这种情况(解决方案: ReentrantReadWriteLock)通过Lock得知线程有没有成功获取锁(解决方案: ReentrantLock,这个synchronized无法办到。1synchronizedJava的关键字,因此是Java的内置特性,是基于JVM层面实现的。而Lock是一个Java接口,是基于JDK层面实现的,通过这个接口可以实现同步访问;2)采用synchronized方式不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而 Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致死锁现象。

6.    Java中的锁

公平锁/非公平锁:公平锁是指多个线程按照申请锁的顺序来获取锁。非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。对于JavaReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁,非公平锁的优点在于吞吐量比公平锁大。Synchronized是一种非公平锁。

可重入锁:可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。可重入锁的一个好处是可以一定程度避免死锁。

如果不是可重入锁,setB可能不会被当前线程执行,造成死锁。

独享锁/共享锁:独享锁是指该锁一次只能被一个线程所持有。共享锁是指该锁可被多个线程所持有。JavaReentrantLock是独享锁。ReadWriteLock其读锁是共享锁,写锁是独享锁。读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。独享锁与共享锁也是通过AQS来实现的,Synchronized是独享锁。

互斥锁/读写锁:独享锁/共享锁是一种广义的说法,互斥锁/读写锁是其具体的实现。互斥锁在Java中的具体实现就是ReentrantLock,读写锁在Java中具体实现是ReadWriteLock.

乐观锁/悲观锁:乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。悲观锁在Java中的使用,就是利用各种锁。乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。

分段锁:分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMapJDK7JDK8HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLockSegment继承了ReentrantLock)。当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作

7.   CASCompare and Swap)对于竞争资源较少的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较小(自旋锁当一个线程获得普通锁后,另一个线程试图获取锁,这个这个线程会挂起阻塞,如果两个线程资源竞争不是特别激烈,而处理器阻塞一个线程引起的线程上下文的切换的代价高于等待资源的代价的时候,那么这个线程可以不放弃CPU时间片,而在原地忙等,自旋锁是一种非阻塞锁),因此可以换取更高的性能。对于资源竞争严重的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized

8.   三个线程顺序执行:a.join()方法 thread.join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程B中调用线程Ajoin()方法。 b.使用synchronized

9.   ThreadLocalThreadLocal一般称为线程本地变量,它是一种特殊的线程绑定机制,将线程与变量绑定在一起,为每一个线程维护一个独立的变量副本。Synchronized是一种互斥同步机制,是为了保证在多线程环境下对于共享资源的访问。而ThreadLocal从本质上讲,无非是提供了一个线程级的变量作用域,它是一种线程封闭技术,ThreadLocal可以理解为将对象的作用范围限制在一个线程上下文中,使得变量的作用域为线程级。ThreadLocal主要解决多线程数据因并发产生不一致的问题,为每个线程的并发访问数据提供一个副本,通过访问副本运行业务。

10.线程的安全性:

当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同这个类都能表现出正确的行为,那么这个类就是线程安全的。

11.线程池:为每个请求创建一个新线程的开销很大,为每个请求创建线程的服务器在创建和销毁线程上花费的时间和消耗的系统资源要比花在处理实际用户请求的时间和资源更多。几种创建线程池的方法:newCachedThreadPool,创建一个可缓存线程池,如果线程池超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程;newFixedThreadPool创建一个指定工作线程数量的线程池,每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始化的最大数,则提交的任务存入到池队列中;newSingleThreadExecutor创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行;newScheduleThreadPool创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行。

12.进程:几乎所有的操作系统都支持运行多个任务,通常一个任务就是一个程序,而一个程序就是一个进程。当一个程序运行时,内部可能包括多个顺序执行流,每个顺序执行流就是一个进程。进程是指处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个单位。当程序进入内存运行时即为进程。进程的三个特点:独立性(进程是系统中独立存在的实体,它可以独立拥有资源,每一个进程都有自己独立的地址空间),动态性(进程和程序的区别在于进程是动态的,进程中有时间的概念,进程具有自己的生命周期和各种不同的状态),并发性(多个进程可以在单个处理器上并发执行互不影响)。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值