Java 多线程初识

1.什么是线程?与进程有什么区别?

线程是指程序在执行过程中,能够执行程序代码的一个执行单元。在java语言中,线程有4中状态:运行、就绪、挂起和结束。

进程是指一段正在执行的程序。在操作系统级别上,程序的执行都是以进程为单位的,而每个进程中通常都会有多个线程互不影响地并发执行。

1.1 为什么要使用多线程

(1)发挥多核CPU的优势

随着工业的进步,现在的笔记本、台式机乃至商用的应用服务器至少也都是双核的,4核、8核甚至16核的也都不少见,如果是单线程的程序,那么在双核CPU上就浪费了50%,在4核CPU上就浪费了75%。单核CPU上所谓的”多线程”那是假的多线程,同一时间处理器只会处理一段逻辑,只不过线程之间切换得比较快,看着像多个线程”同时”运行罢了。多核CPU上的多线程才是真正的多线程,它能让你的多段逻辑同时工作,多线程,可以真正发挥出多核CPU的优势来,达到充分利用CPU的目的。

(2)防止阻塞

从程序运行效率的角度来看,单核CPU不但不会发挥出多线程的优势,反而会因为在单核CPU上运行多线程导致线程上下文的切换,而降低程序整体的效率。但是单核CPU我们还是要应用多线程,就是为了防止阻塞。试想,如果单核CPU使用单线程,那么只要这个线程阻塞了,比方说远程读取某个数据吧,对端迟迟未返回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止运行了。多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行。

(3)使用多线程能简化程序的结构,使程序便于理解和维护。一个非常复杂的进程可以分成多个线程来执行。

(4)与进程相比,线程的创建和切换开销更小。由于启动一个新的线程必须给这个线程分配独立的地址空间,建立许多数据结构来维护线程代码段、数据段等信息,而运行于同一进程内的线程共享代码段、数据段,线程的启动或切换的开销比进程要少很多。同时多线程在共享数据方面效率非常高。

2 同步和异步有什么区别

例如多个线程同时对同一个数据进行写操作,即当线程A需要使用某个资源时,如果这个资源正在被B线程使用,同步机制就会让线程A一只等待下去,直到线程B结束对这个数据资源的使用后,线程A才能使用这个资源。由此可见,同步可以保证资源的安全。

要想实现同步操作,必须要获得每一个线程对象的锁。获得它可以保证同一时刻只有一个线程能够进入临界区(访问互斥资源的代码块),并且在这个锁被释放之前,其他线程就不能再进入这个临界区。如果还有其他线程想要获得该对象的锁,只能进入等待队列等待。只有当拥有该对象锁的线程退出临界区,锁才会被释放,等待队列中优先级最高的线程才能获得该锁,从而进入共享代码区。

java语言在同步机制中提供了语言级的支持,可以通过使用synchronized关键字来实现同步,但该方法并非“万金油”,它是以很大的系统开销作为代价的,有时候甚至可能造成死锁,所以,同步控制并非越多越好,要尽量避免无谓的同步控制。实现同步的方式有两种:一是利用同步代码块来实现同步;二是利用同步方法来实现同步。

异步与非阻塞类似,由于每个线程都包含了运行时自身所需要的数据或方法,因此,在进行输入输出处理时,不必关心其他线程的状态或行为,也不必等到输入输出处理完毕才返回。当应用程序对象上调用了一个需要话费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,异步能够提高程序的效率。

3 如何实现java多线程

(1)继承Thread类创建线程类

重写父类run( )方法

[java] view plain copy
public class thread1 extends Thread {  
   public void run() {  
       for (int i = 0; i < 10000; i++) {  
           System.out.println("线程一"+i);  
       }  
   }  
   
  public static void main(String[] args) {  
       thread1 th1 = new thread1();  
       thread1 th2 = new thread1();  
       th1.run();  
       th2.run();  
   }  
}

run( )方法只是普通的方法,是顺序执行的,虽然我们是写了一个线程,但是并没有体现出多线程的意义。为了体现多线程,应该使用start( )方法来启动线程,start()方法会自动调用run( )方法。所以上面的代码改为:

[java] view plain copy
public class thread1 extends Thread {    
    public void run() {  
        for (int i = 0; i < 10000; i++) {  
            System.out.println("线程一"+i);  
        }  
    }  
    
public static void main(String[] args) {  
        thread1 th1 = new thread1();  
        thread1 th2 = new thread1();  
        th1.start();  
        th2.start();  
    }  
  
} 

通过start( )方法启动的线程。不管th1.start( )调用的run( )方法是否执行完,都继续执行th2.start( )。如果下面有别的代码也同样不需要等待th2.start( )执行完而继续执行。

(2)实现Runnable接口创建线程类

[java] view plain copy
public class thread2 implements Runnable {  
    public String ThreadName;  
        
    public thread2(String tName){  
        ThreadName = tName;  
    }  
        
    public void run() {  
        for (int i = 0; i < 10000; i++) {  
            System.out.println(ThreadName);  
        }  
    }  
        
    public static void main(String[] args) {  
        thread2 th1 = new thread2("线程A");  
        thread2 th2 = new thread2("线程B");  
        Thread myth1 = new Thread(th1);  
        Thread myth2 = new Thread(th2);  
        myth1.start();  
        myth2.start();  
    }  
} 

其实Thread类也是实现了Runnable接口,代表了一个线程的实例,并且启动线程唯一的方法就是通过Thread类的start( )方法。所以第二种实现多线程的方法必须要提拔个工艺new一个Thread类去启动线程。同样的,也不能只是运行run( )方法,因为这样只是一个线程。

(3)使用ExecutorService、Callable、Future实现由返回结果的多线程

这种方式是之前没有尝试过的,ExecutorService是一个线程池接口,可返回值的任务必须实现Callable接口,无返回值得任务必须实现Runnable接口。执行Callable任务后,可以获取一个Future对象,在该对象上调用get就可以获取到Callable任务返回的Object了,再结合ExecutorService接口就可以实现有返回结果的多线程了。

[java] view plain copy
/** 
 * 有返回值的线程 
 */  
@SuppressWarnings("unchecked")  
class Test {  
    public static void main(String[] args) throws ExecutionException,  
            InterruptedException {  
        System.out.println("----程序开始运行----");  
        Date date1 = new Date();  
  
        int taskSize = 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 implements Callable<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 + "毫秒】";  
    }  
}  

总结

实现多线程的几种方式,建议使用runable实现,不管如何最终都需要thread.start( )来启动线程。

4 start()方法和run()方法的区别

通常,系统通过调用线程类的start()方法来启动一个线程,此时线程处于就绪状态,而非运行状态,也就意味着这个线程可以被JVM来调度执行。在调度过程中,JVM通过调用线程类的run()方法来完成实际的操作,当run()方法结束后,此线程就会终止。

如果直接调用线程类的run()方法,这会被当作一个普通的函数调用,程序中依然只有主线程这一个线程,也就是说,start()方法能够异步的调用run()方法,但是直接调用run()方法却是同步的,因此也就无法达到多线程的目的。

只有调用了start()方法,才会表现出多线程的特性,不同线程的run()方法里面的代码交替执行。如果只是调用run()方法,那么代码还是同步执行的,必须等待一个线程的run()方法里面的代码全部执行完毕之后,另外一个线程才可以执行其run()方法里面的代码。

5 多线程同步的实现方法有哪些

1.synchronized关键字

(1)synchronized方法

(2)synchronized代码块

2.volatile关键字

3.Lock

6 多线程同步,synchronized与Lock有什么异同

(1)synchronized

存在层次:Java的关键字,在jvm层面上

锁的释放:1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁

锁的获取:假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待

锁状态:无法判断

锁类型:可重入 不可中断 非公平

性能:少量同步

(2)Lock

存在层次:是一个类(还多了锁投票、定时锁、等候和中断锁等)

锁的释放:在finally中必须释放锁,不然容易造成线程死锁

锁的获取:分情况而定,Lock有多个锁获取的方式,具体下面会说道,大致就是可以尝试获得锁,线程可以不用一直等待

锁状态:可以判断

锁类型:可重入 可判断 可公平(两者皆可)

性能:大量同步

(3)volatile

  • volatile关键字为域变量的访问提供了一种免锁机制
  • 使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新
  • 因此每次使用该域就要重新计算,而不是使用寄存器中的值
  • volatile不会提供任何原子操作,它也不能用来修饰final类型的变量,因此volatile不能代替synchronized(不建议使用)

7 wait()方法与notify()方法

wait():释放占有的对象锁,线程进入等待池,释放cpu,而其他正在等待的线程即可抢占此锁,获得锁的线程即可运行程序。而sleep()不同的是,线程调用此方法后,会休眠一段时间,休眠期间,会暂时释放cpu,但并不释放对象锁。也就是说,在休眠期间,其他线程依然无法进入此代码内部。休眠结束,线程重新获得cpu,执行代码。wait()和sleep()最大的不同在于wait()会释放对象锁,而sleep()不会!

notify(): 该方法会唤醒因为调用对象的wait()而等待的线程,其实就是对对象锁的唤醒,从而使得wait()的线程可以有机会获取对象锁。调用notify()后,并不会立即释放锁,而是继续执行当前代码,直到synchronized中的代码全部执行完毕,才会释放对象锁。JVM则会在等待的线程中调度一个线程去获得对象锁,执行代码。需要注意的是,wait()和notify()必须在synchronized代码块中调用。

notifyAll()则是唤醒所有等待的线程。

8 sleep方法和wait方法有什么区别

1、sleep()方法

sleep()使当前线程进入停滞状态(阻塞当前线程),让出CUP的使用、目的是不让当前线程独自霸占该进程所获的CPU资源,以留一定时间给其他线程执行的机会。

sleep()是Thread类的Static(静态)的方法。因此他不能改变对象的机锁,所以当在一个Synchronized块中调用Sleep()方法是,线程虽然休眠了,但是对象的机锁并木有被释放,其他线程无法访问这个对象(即使睡着也持有对象锁)。

在sleep()休眠时间期满后,该线程不一定会立即执行,这是因为其它线程可能正在运行而且没有被调度为放弃执行,除非此线程具有更高的优先级。

2、wait()方法

wait()方法是Object类里的方法。当一个线程执行到wait()方法时,它就进入到一个和该对象相关的等待池中,同时失去(释放)了对象的机锁(暂时失去机锁,wait(long timeout)超时时间到后还需要返还对象锁)。其他线程可以访问。

wait()使用notify或者notifyAlll或者指定睡眠时间来唤醒当前等待池中的线程。

notify()和notifyAll()方法只是唤醒等待该对象的monitor(即锁)的线程,并不决定哪个线程能够获取到monitor(即锁)。

wait()必须放在synchronized block中,否则会在program runtime时抛出java.lang.IllegalMonitorStateException异常。

3、所以sleep()和wait()方法的最大区别是

sleep()睡眠时,保持对象锁,仍然占有该锁(其他线程无法访问这个对象)。

而wait()睡眠时,释放对象锁(其他线程可以访问)。

但是wait()和sleep()都可以通过interrupt()方法打断线程的暂停状态,从而使线程立刻抛出InterruptedException(但不建议使用该方法)。

7 终止线程的方法有哪些

1.使用退出标志终止线程

一般run()方法执行完,线程就会正常结束,然而,常常有些线程是伺服线程。它们需要长时间的运行,只有在外部某些条件满足的情况下,才能关闭这些线程。使用一个变量来控制循环,例如:最直接的方法就是设一个boolean类型的标志,并通过设置这个标志为true或false来控制while循环是否退出,代码示例:

public class ThreadSafe extends Thread {
    public volatile boolean exit = false; 
        public void run() { 
        while (!exit){
            //do something
        }
    } 
}

定义了一个退出标志exit,当exit为true时,while循环退出,exit的默认值为false.在定义exit时,使用了一个Java关键字volatile,这个关键字的目的是使exit同步,也就是说在同一时刻只能由一个线程来修改exit的值.

2.使用interrupt()方法中断当前线程

使用interrupt()方法来中断线程有两种情况:

1.线程处于阻塞状态,如使用了sleep,同步锁的wait,socket中的receiver,accept等方法时,会使线程处于阻塞状态。当调用线程的interrupt()方法时,会抛出InterruptException异常。阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后break跳出循环状态,从而让我们有机会结束这个线程的执行。通常很多人认为只要调用interrupt方法线程就会结束,实际上是错的, 一定要先捕获InterruptedException异常之后通过break来跳出循环,才能正常结束run方法。

代码示例:

public class ThreadSafe extends Thread {
    public void run() { 
        while (true){
            try{
                    Thread.sleep(5*1000);//阻塞5妙
                }catch(InterruptedException e){
                    e.printStackTrace();
                    break;//捕获到异常之后,执行break跳出循环。
                }
        }
    } 
}

2.线程未处于阻塞状态,使用isInterrupted()判断线程的中断标志来退出循环。当使用interrupt()方法时,中断标志就会置true,和使用自定义的标志来控制循环是一样的道理。
代码示例:

public class ThreadSafe extends Thread {
    public void run() { 
        while (!isInterrupted()){
            //do something, but no throw InterruptedException
        }
    } 
}

为什么要区分进入阻塞状态和和非阻塞状态两种情况了,是因为当阻塞状态时,如果有interrupt()发生,系统除了会抛出InterruptedException异常外,还会调用interrupted()函数,调用时能获取到中断状态是true的状态,调用完之后会复位中断状态为false,所以异常抛出之后通过isInterrupted()是获取不到中断状态是true的状态,从而不能退出循环,因此在线程未进入阻塞的代码段时是可以通过isInterrupted()来判断中断是否发生来控制循环,在进入阻塞状态后要通过捕获异常来退出循环。因此使用interrupt()来退出线程的最好的方式应该是两种情况都要考虑:

代码示例:

public class ThreadSafe extends Thread {
    public void run() { 
        while (!isInterrupted()){ //非阻塞过程中通过判断中断标志来退出
            try{
                Thread.sleep(5*1000);//阻塞过程捕获中断异常来退出
            }catch(InterruptedException e){
                e.printStackTrace();
                break;//捕获到异常之后,执行break跳出循环。
            }
        }
    } 
}

3.使用stop方法终止线程

程序中可以直接使用thread.stop()来强行终止线程,但是stop方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,不安全主要是:thread.stop()调用之后,创建子线程的线程就会抛出ThreadDeatherror的错误,并且会释放子线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。因此,并不推荐使用stop方法来终止线程。

9 什么是守护线程

10 线程的几种状态

新建状态:新建线程对象,并没有调用start()方法之前

就绪状态:调用start()方法之后线程就进入就绪状态,但是并不是说只要调用start()方法线程就马上变为当前线程,在变为当前线程之前都是为就绪状态。值得一提的是,线程在睡眠和挂起中恢复的时候也会进入就绪状态哦。

运行状态:线程被设置为当前线程,开始执行run()方法。就是线程进入运行状态

阻塞状态:线程被暂停,比如说调用sleep()方法后线程就进入阻塞状态

死亡状态:线程执行结束

11.锁类型

可重入锁:在执行对象中所有同步方法不用再次获得锁

可中断锁:在等待获取锁过程中可中断

公平锁: 按等待获取锁的线程的等待时间进行获取,等待时间长的具有优先获取锁权利

读写锁:对资源读取和写入的时候拆分为2部分处理,读的时候可以多线程一起读,写的时候必须同步地写

10 join()方法的作用是什么

Thread类中的join方法的主要作用就是同步,它可以使得线程之间的并行执行变为串行执行。

11 volatile关键字的作用

volatile是一个类型修饰符(type specifier)。它是被设计用来修饰被不同线程访问和修改的变量。确保本条指令不会因编译器的优化而省略,且要求每次直接读值。它能够使变量在值发生改变时能尽快地让其他线程知道。

12 如何在2个线程之间共享数据

13 为什么要使用线程池

避免频繁地创建和销毁线程,达到线程对象的重用。另外,使用线程池还可以根据项目灵活地控制并发的数目。

14 死锁

死锁指的是两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,如果无外力作用,它们都将无法推进。

当线程需要同时持有多个锁时,有可能产生死锁。

大部分代码并不容易产生死锁,死锁可能在代码中隐藏相当长的时间,等待不常见的条件地发生,但即使是很小的概率,一旦发生,便可能造成毁灭性的破坏。避免死锁是一件困难的事,遵循以下原则有助于规避死锁:

1、只在必要的最短时间内持有锁,考虑使用同步语句块代替整个同步方法。

2、尽量编写不在同一时刻需要持有多个锁的代码,如果不可避免,则确保线程持有第二个锁的时间尽量短暂。

3、创建和使用一个大锁来代替若干小锁,并把这个锁用于互斥,而不是用作单个对象的对象级别锁。

15 怎么唤醒一个阻塞的线程

如果线程是因为调用了wait()、sleep()或者join()方法而导致的阻塞,可以中断线程,并且通过抛出InterruptedException来唤醒它;如果线程遇到了IO阻塞,无能为力,因为IO是操作系统实现的,Java代码并没有办法直接接触到操作系统。

16 同步方法和同步块,哪个是更好的选择

同步块,这意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代码的效率。请知道一条原则:同步的范围越少越好。

借着这一条,我额外提一点,虽说同步的范围越少越好,但是在Java虚拟机中还是存在着一种叫做锁粗化的优化方法,这种方法就是把同步范围变大。这是有用的,比方说StringBuffer,它是一个线程安全的类,自然最常用的append()方法是一个同步方法,我们写代码的时候会反复append字符串,这意味着要进行反复的加锁->解锁,这对性能不利,因为这意味着Java虚拟机在这条线程上要反复地在内核态和用户态之间进行切换,因此Java虚拟机会将多次append方法调用的代码进行一个锁粗化的操作,将多次的append的操作扩展到append方法的头尾,变成一个大的同步块,这样就减少了加锁–>解锁的次数,有效地提升了代码执行的效率。

17 什么是乐观锁和悲观锁

(1)乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-设置这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。

(2)悲观锁:还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。

18 高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?

(1)高并发、任务执行时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文的切换

(2)并发不高、任务执行时间长的业务要区分开看:

a)假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以加大线程池中的线程数目,让CPU处理更多的业务

b)假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换

(3)并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考(2)。最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦。

原文地址:http://www.dearweb.cn/houduan/dw-117.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值