黑马程序员--Java 多线程与并发总结

时间:2023-02-20 13:07:02

------Java培训、Android培训、iOS培训、.Net培训、期待与您交流!---------

      今天重新复习了《Java编程思想》中的第21章 并发及张孝祥_Java多线程视频教程,写一点总结。

    到目前为止,学到的都是关于顺序编程的知识,即程序中所有的事物在任意时刻都只能执行一个步骤。并发编程的令人困惑的一个主要原因是:使用并发时需要解决的问题有多个,而实现并发的方式也有多种,并在两者之间没有明显的映射关系。

首先了解一下进程和线程,这里我转载了一篇文章(图片说明),很形象生动。进程与线程的关系

在java中要想实现多线程,有两种手段,一种是继续Thread类,另外一种是实现Runable接口。今天看到老师用匿名类实现的,感觉挺有意思的,就敲了一边代码,和大家一起分享:

package Java多线程及并发;

public class ThreadDemo {
public static void main(String[] args) {
//相当于继承Thread类
Thread trd1=new Thread(){
public void run(){
while(true){
try {
Thread.sleep(1000);
System.out.println("线程的名字是:"+Thread.currentThread().getName());
}
catch (InterruptedException e)
{
e.printStackTrace();
}

}
}
};
//运行此线程
trd1.start();

//使用Runabled
Thread trd2=new Thread(new Runnable() {
public void run() {
while(true){
try {
Thread.sleep(1500);
System.out.println("线程的名字是:"+Thread.currentThread().getName());
}
catch (InterruptedException e)
{
e.printStackTrace();
}

}
}
});
trd2.start();
}
}

以上代码都是采用匿名类的方法实现的,而不是一般的继承和实现接口。

Thread类主要有两种构造方法 Thread()和Thread(Runnable target)。最重要的方法应该是 run()了, 如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。Runnable接口应该由那些打算通过某一线程执行其实例的类来实现。类必须定义一个称为run 的无参数方法。设计该接口的目的是为希望在活动时执行代码的对象提供一个公共协议。例如,Thread 类实现了 Runnable。激活的意思是说某个线程已启动并且尚未停止。 

首先来看一个简单的例子:

package Java多线程及并发;

class ThreadDemo2 extends Thread {
public ThreadDemo2() {

}

public ThreadDemo2(String threadName) {
this.threadName = threadName;
}

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

public static void main(String[] args) {
ThreadDemo2 trdA=new ThreadDemo2("A");
ThreadDemo2 trdB=new ThreadDemo2("B");
trdA.start();
trdB.start();
}

private String threadName;
}

执行的结果是:

A我拍 : 0
B我拍 : 0
A我拍 : 1
B我拍 : 1
A我拍 : 2
B我拍 : 2
A我拍 : 3
B我拍 : 3
A我拍 : 4
B我拍 : 4

假如把start方法换成run方法呢?

trdA.run();
trdA.run();

输出的结果是:

A我拍 : 0
A我拍 : 1
A我拍 : 2
A我拍 : 3
A我拍 : 4
A我拍 : 0
A我拍 : 1
A我拍 : 2
A我拍 : 3
A我拍 : 4

就变成了顺序执行了,为什么呢? 

用start方法来启动线程,真正实现了多线程运行,这时无需等待run方法体代码执行完毕而直接继续执行下面的代码。通过调用Thread类的start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到spu时间片,就开始执行run()方法,这里方法run()称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程随即终止。

run()方法只是类的一个普通方法而已,如果直接调用Run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到写线程的目的。

总结:调用start方法方可启动线程,而run方法只是thread的一个普通方法调用,还是在主线程里执行。

那实现线程的方法有两种,是选择继承Thread还是实现Runnable接口?下面看代码:

package Java多线程及并发;

class ThreadDemo2 extends Thread {
public ThreadDemo2() {

}

public ThreadDemo2(String threadName) {
this.threadName = threadName;
}

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

public static void main(String[] args) {
ThreadDemo2 trdA=new ThreadDemo2("A");
ThreadDemo2 trdB=new ThreadDemo2("B");
ThreadDemo2 trdC=new ThreadDemo2("C");
trdA.start();
trdB.start();
trdC.start();
}

private String threadName;
private int counter=5;
}
输出结果是:

A:counter= 5
C:counter= 5
B:counter= 5
C:counter= 4
C:counter= 3
C:counter= 2
A:counter= 4
C:counter= 1
B:counter= 4
A:counter= 3
A:counter= 2
B:counter= 3
B:counter= 2
A:counter= 1
B:counter= 1


下面实现Runnable接口:

package Java多线程及并发;

class BuyTicket implements Runnable
{
private int count=5;
public void run()
{
for (int i=0; i<=20; i++) {
if (this.count > 0) {
System.out.println(Thread.currentThread().getName()+"正在出售车票: " + count--);
}
}
}

}
public class ThreadDemo3 {
public static void main(String[] args) {
BuyTicket bt = new BuyTicket();
new Thread(bt,"①号窗口").start();
new Thread(bt,"②号窗口").start();
new Thread(bt,"③号窗口").start();

}

}

输出结果是:

①号窗口正在出售车票: 5
③号窗口正在出售车票: 3
②号窗口正在出售车票: 4
③号窗口正在出售车票: 1
①号窗口正在出售车票: 2

如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。因为实现接口Runnable的类,可以再多克线程中同时运行,但是数据是共享的,因为是同一个类。

实现Runnable接口比继承Thread类所具有的优势:

1):适合多个相同的程序代码的线程去处理同一个资源(共享)

2):可以避免java中的单继承的限制

3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立。

4):更符合面向对象的思想。

    提醒一下大家:main方法其实也是一个线程。在java中所以的线程都是同时启动的,至于什么时候,哪个先执行,完全看谁先得到CPU的资源。java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个jVM就是在操作系统中启动了一个进程。

   下面介绍一下Thread类中的主要函数:

 1). boolean  isAlive()   //测试线程是否处于活动状态。

 2). void sleep(long millis)  //线程休眠

 3). void interrupt()  //线程中断

 4). void setPriority(int newPriority)  //线程的优先级,不要误以为优先级越高就先执行。谁先执行还是取决于谁先去的CPU的资源、

                                                   //主线程的优先级是5.

 5). void yield() //暂停当前正在执行的线程对象,并执行其他线程


    下面就讲解同步与死锁的问题,请看代码:

package Java多线程及并发;

public class ThreadDemo4 implements Runnable {
public void run() {
for(int i=0;i<10;++i){
if(count>0){
try{
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"输出的数:"+count--);
}
}
}

public static void main(String[] args) {
ThreadDemo4 td=new ThreadDemo4();
Thread trd1=new Thread(td);
Thread trd2=new Thread(td);
Thread trd3=new Thread(td);
trd1.start();
trd2.start();
trd3.start();
}
private int count=5;
}

输出的结果是:

Thread-0输出的数:5
Thread-1输出的数:4
Thread-2输出的数:4
Thread-1输出的数:3
Thread-2输出的数:3
Thread-0输出的数:2
Thread-0输出的数:1
Thread-2输出的数:-1
Thread-1输出的数:0

     这里出现了负数,但是count>0。因为三个线程同时运行, count数值不能保证统一性。 如果想解决这种问题,就需要使用同步。所谓同步就是在统一时间段中只有有一个线程运行, 其他的线程必须等到这个线程结束之后才能继续执行。使用 synchronized(同步对象){  //需要同步的代码  }  同步代码块。

package Java多线程及并发;

public class ThreadDemo4 implements Runnable {
public void run() {
for(int i=0;i<10;++i){
synchronized (this) { //synchronized(同步对象){同步内容} //<span style="line-height: 23px; color: rgb(51, 51, 51); font-size: 13px; font-family: 宋体;">一般都把当前对象</span><span lang="EN-US" style="line-height: 23px; color: rgb(51, 51, 51); font-family: Georgia, 'Times New Roman', Times, sans-serif; font-size: 13px;">this</span><span style="line-height: 23px; color: rgb(51, 51, 51); font-size: 13px; font-family: 宋体;">作为同步对象</span>
if(count>0){
try{
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"输出的数:"+count--);
}
}
}
}

public static void main(String[] args) {
ThreadDemo4 td=new ThreadDemo4();
Thread trd1=new Thread(td,"线程1");
Thread trd2=new Thread(td,"线程2");
Thread trd3=new Thread(td,"线程3");
trd1.start();
trd2.start();
trd3.start();
}
private int count=5;
}

输出的结果:

线程1输出的数:5
线程1输出的数:4
线程2输出的数:3
线程2输出的数:2
线程2输出的数:1

也可以使用 synchronized  方法返回类型 方法名(参数列表) {    //  其他代码    }同步方法

package Java多线程及并发;

public class ThreadDemo4 implements Runnable {
public void run() {
for(int i=0;i<10;++i){
//synchronized (this) { //synchronized(同步对象){同步内容} }
tongbu();
}
}

public synchronized void tongbu(){
if(count>0){
try{
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"输出的数:"+count--);
}

}

public static void main(String[] args) {
ThreadDemo4 td=new ThreadDemo4();
Thread trd1=new Thread(td,"线程1");
Thread trd2=new Thread(td,"线程2");
Thread trd3=new Thread(td,"线程3");
trd1.start();
trd2.start();
trd3.start();
}
private int count=5;
}

输出的结果是:

线程1输出的数:5
线程3输出的数:4
线程3输出的数:3
线程3输出的数:2
线程3输出的数:1

    下面我总结了Java多线程的面试题,以便更加深入的理解并发机制。

1).什么是多线程编程?什么时候使用?

    多线程一般用于当一个程序需要同时做一个以上的任务。多线程通常用于GUI交互程序。一个新的线程被创建做一些耗时的工作,当主线程保持界面与用户的交互。 

2).wait()方法和sleep()方法有什么不同?  

    sleep()方法执行后仍然拥有线程,只是延时。而wait方法放弃了线程控制,其它线程可以运行,想要再次运行是要重新开始。

3).Thread.start ()与 Thread.run ()有什么区别? 

     Thread.start ()方法(native)启动线程,使之进入就绪状态,当 cpu分配时间该线程时,由 JVM 调度执行 run ()方法。

4).解释实现多线程的几种方法?

      Java 线程可以实现 Runnable 接口或者继承 Thread 类来实现,当你打算多重继承时,优先选择实现 Runnable。

5)..为什么需要 run ()和 start ()方法,我们可以只用 run ()方法来完成任务吗? 

     我们需要 run ()&start ()这两个方法是因为 JVM 创建一个单独的线程不同于普通方法的调用,所以这项工作由线程的 start 方法来完成,start 由本地方法实现,需要显示地被调用,使用这俩个方法的另外一个好处是任何一个对象都可以作为线程运行,只要实现了 Runnable 接口,这就避免因继承了 Thread 类而造成的 Java 的多继承问题。 

6).什么是 ThreadLocal 类,怎么使用它? 

   ThreadLocal 是一个线程级别的局部变量,并非“本地线程”。ThreadLocal 为每个使用该变量的线程提供了一个独立的变量副本,每个线程修改副本时不影响其它线程对象的副本(译者注)。 
下面是线程局部变量(ThreadLocal variables)的关键点: 
一个线程局部变量(ThreadLocal variables)为每个线程方便地提供了一个单独的变量。 
ThreadLocal 实例通常作为静态的私有的(private static)字段出现在一个类中,这个类用来关联一个线程。 
当多个线程访问 ThreadLocal 实例时,每个线程维护 ThreadLocal 提供的独立的变量副本。 
常用的使用可在 DAO 模式中见到,当 DAO 类作为一个单例类时,数据库链接(connection)被每一个线程独立的维护,互不影响。(基于线程的单例) 

7).什么时候抛出 InvalidMonitorStateException 异常,为什么? 

    调用 wait ()/notify ()/notifyAll ()中的任何一个方法时,如果当前线程没有获得该对象的锁,那么就会抛出 IllegalMonitorStateException 的异常(也就是说程序在没有执行对象的任何同步块或者同步方法时,仍然尝试调用 wait ()/notify ()/notifyAll ()时)。由于该异常是 RuntimeExcpetion 的子类,所以该异常不一定要捕获(尽管你可以捕获只要你愿意).作为 RuntimeException,此类异常不会在 wait (),notify (),notifyAll ()的方法签名提及。

8)..在静态方法上使用同步时会发生什么事? 

    同步静态方法时会获取该类的“Class”对象,所以当一个线程进入同步的静态方法中时,线程监视器获取类本身的对象锁,其它线程不能进入这个类的任何静态同步方法。它不像实例方法,因为多个线程可以同时访问不同实例同步实例方法。 

9).在一个对象上两个线程可以调用两个不同的同步实例方法吗?

不能,因为一个对象已经同步了实例方法,线程获取了对象的对象锁。所以只有执行完该方法释放对象锁后才能执行其它同步方法。看下面代码示例非常清晰:Common 类有 synchronizedMethod1()和 synchronizedMethod2()方法,MyThread 调用这两个方法。 

package Java多线程及并发;

class Common{
public synchronized void synchronizedMethod1() {
System.out.println ("同步方法 1");
try {
Thread.sleep (1000);
} catch (InterruptedException e) {
e.printStackTrace ();
}
System.out.println ("执行同步方法1");
}

public synchronized void synchronizedMethod2() {
System.out.println ("同步方法 2");
try {
Thread.sleep (1000);
} catch (InterruptedException e) {
e.printStackTrace ();
}
System.out.println ("执行同步方法2");
}
}
public class MyThread extends Thread {
private int id = 0;
private Common common;

public MyThread (String name, int no, Common object) {
super(name);
common = object;
id = no;
}

public void run () {
System.out.println ("运行线程:" + this.getName ());
try {
if (id == 0) {
common.synchronizedMethod1();
} else {
common.synchronizedMethod2();
}
} catch (Exception e) {
e.printStackTrace ();
}
}

public static void main (String[] args) {
Common c = new Common ();
MyThread t1 = new MyThread ("线程1", 0, c);
MyThread t2 = new MyThread ("线程2", 1, c);
t1.start ();
t2.start ();
}
}

执行的结果是:

运行线程:线程1
运行线程:线程2
同步方法 1
执行同步方法1
同步方法 2
执行同步方法2

 10).在main函数里启动一个定时器,是不是main函数执行完整个程序就退出了,包括那个定时器

public class MainThreadTest {

public static void main(String[] args) {
new Timer().schedule(new TimerTask(){

@Override
public void run() {
System.out.println("Timer thread is running...");
}

}, 500, 500);

System.out.println("Main thread ends!");
}
}
输出的结果是:

Main thread ends!
Timer thread is running...
Timer thread is running...
Timer thread is running...

Java中的main线程是不是最后一个退出的线程,它是一个用户线程,执行完毕就退出了。