java基础之线程(重点)

一、线程与进程

(一)进程产生的原因:

由于cpu与其他pc资源(RAM,显卡、光驱、键盘等)之间速度的不协调,为了提高资源利用率,提出了多任务系统,得益于cpu的计算速度,实质上是多个任务轮流使用cpu资源,由于速度很快,感觉是同时进行的。

任务执行需要具备执行环境,即程序执行所需的各种pc资源(显卡、RAM等),将其称之为程序上下文,要实现程序可以同时执行,就需要不断轮换,为了从当前状态继续执行下去,计算机需要保存之前的程序上下文,因此有了进程,用进程来表述程序当前上下文的状态信息(内存信息,变量值,任务ID等)。

因此,程序在运行时,需要加载程序上下文,执行程序,保存程序上下文,调入另一个程序的上下文,执行程序。。。

(二)线程产生的原因:

当程序增多时,耗时就会增大,为了进一步提高资源利用率,在进程中,引入了线程,线程只是cpu轮流调度的单位。例如:要运行程序A,实际上分成a,b,c等多个块组合而成,具体执行情况:程序A得到CPU——》CPU加载程序上下文,开始执行程序A的a小段,b小段,c小段,最后cpu保存A的上下文。这里a,b,c的执行共享了A的上下文,其中a,b,c就是线程,即更加细分了CPU的使用时间段。

(三)进程的概念

进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1--n个线程。

注意:程序是一个静态的概念,机器上的一个class文件或者exe文件就是一个程序。进程是程序的一次动态执行,首先将class文件加载到代码区,这时进程还没有开始执行,但进程已经产生了。平常说的进程的执行指的是主线程执行即main()方法的执行。

(四)线程的概念

线程:线程是进程中的一个执行流程,同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。(主要用于解决共享资源的有效有序使用,保证运行结果正确可控)。

线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。

二、线程的创建与执行

线程有三种创建方法:

(一)第一种方法:继承Thread类

继承java.lang.Thread类,子类必须重写run()方法,该方法作为新线程的入口点,同时,必须调用start()方法才能执行。(start()方法是通知cpu,线程我准备好了)

构造方法:publicThread(Runnable  target)

注意:Thread类实际上是实现了Runnable接口的类。

(二)第二种方法:实现Runnable接口(推荐)

实现java.lang.Runnable接口,实现public  void   run()方法,该方法用于定义线程的运行体。(推荐使用)

注意:使用Runnable接口可以为多个线程提供共享的数据。

在run()方法中可以使用Thread类的静态方法:currentThread()方法:获取当前线程的引用

(三)第三中方法:利用Callable接口和FutureTask类实现

  • 1. 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。

  • 2. 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。

  • 3. 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。

  • 4. 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。

public class CallableThreadTest implements Callable<Integer> {
    public static void main(String[] args)  
    {  
        CallableThreadTest ctt = new CallableThreadTest();  
        FutureTask<Integer> ft = new FutureTask<>(ctt);  
        for(int i = 0;i < 100;i++)  
        {  
            System.out.println(Thread.currentThread().getName()+" 的循环变量i的值"+i);  
            if(i==20)  
            {  
                new Thread(ft,"有返回值的线程").start();  
            }  
        }  
        try  
        {  
            System.out.println("子线程的返回值:"+ft.get());  
        } catch (InterruptedException e)  
        {  
            e.printStackTrace();  
        } catch (ExecutionException e)  
        {  
            e.printStackTrace();  
        }  
  
    }
    @Override  
    public Integer call() throws Exception  
    {  
        int i = 0;  
        for(;i<100;i++)  
        {  
            System.out.println(Thread.currentThread().getName()+" "+i);  
        }  
        return i;  
    }  
}

(四)总结:

  • 1. 采用实现 Runnable、Callable 接口的方式创见多线程时,线程类只是实现了 Runnable 接口或 Callable 接口,还可以继承其他类。

  • 2. 使用继承 Thread 类的方式创建多线程时,编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread() 方法,直接使用 this 即可获得当前线程。

三、线程的状态

(一)线程状态

1、线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。

技术分享

 

  • 新建状态:

    使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。

  • 就绪状态:

    当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。

  • 运行状态:

    如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。

  • 阻塞状态:

    如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:

    • 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。

    • 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。

    • 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。

  • 死亡状态:

    一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。

(二)线程控制方法:

1、isAlive()方法:判断线程是否还活着,即线程是否终止

2、getPriority()方法:获得线程的优先级数值。

3、setPriority()方法:设置线程的优先级数值。

注意:线程的优先级无法保证线程的执行次序,只不过,优先级高的线程获得CPU资源的概率比较大,获得CPU的执行时间越多。优先级低的也有机会执行。优先级用1-10之间的整数表示,默认是5(可省略),数值越大,优先级越高。

4、Thread.sleep():将当前线程睡眠指定毫秒数

5、join()方法:调用某线程的该方法,将当前线程和该线程合并,即等待该线程结束,再恢复当前线程的运行

6、yield()方法:让出CPU,当前线程进入就绪队列等待调度。

7、wait()方法:当前线程进入阻塞状态,让出CPU控制权,当前线程进入对象的wait pool。

8、notify()方法、notifyAll()方法:唤醒对象的wait  pool 中的一个或者所有等待的线程

(三)sleep()和wait()的区别(重点)

1、每个对象都有一个锁来控制同步访问,Synchronized关键字可以和对象的锁交互,来实现同步方法或同步块。sleep()方法正在执行的线程主动让出CPU(然后CPU就可以去执行其他任务),在sleep指定时间后CPU再回到该线程继续往下执行(注意:sleep方法只让出了CPU,而并不会释放同步资源锁!!!);wait()方法则是指当前线程让自己暂时退让出同步资源锁,以便其他正在等待该资源的线程得到该资源进而运行,只有调用了notify()方法,之前调用wait()的线程才会解除wait状态,可以去参与竞争同步资源锁,进而得到执行。(注意:notify的作用相当于叫醒睡着的人,而并不会给他分配任务,就是说notify只是让之前调用wait的线程有权利重新参与线程的调度);

2、sleep()方法可以在任何地方使用;wait()方法则只能在同步方法或同步块中使用;

3、sleep()是线程线程类(Thread)的方法,调用会暂停此线程指定的时间,但监控依然保持,不会释放对象锁,到时间自动恢复;wait()是Object的方法,调用会放弃对象锁,进入等待队列,待调用notify()/notifyAll()唤醒指定的线程或者所有线程,才会进入锁池,不再次获得对象锁才会进入运行状态;

(四)sleep / join / yield 方法

1、sleep(long   millis)方法:是Thread类的静态方法,睡眠指定的毫秒数。一秒=1000毫秒

注意:sleep(long  millis)方法是静态方法,在那个线程中调用该方法,就让那个线程睡眠。

(1)interrupt()方法:中断线程,会抛出一个异常,进行处理(太粗暴,不推荐使用)

(2)stop()方法:中断线程,更加粗暴,直接杀死线程。(已过时,尽量不要使用)(重点)

注意:中断是一个状态,interrupt()方法只是将这个状态置为true而已。它并不像stop()方法那样会中断一个正在运行的线程,线程会不时地检测中断标识位,以判断线程是否应该被中断(中断标识值是否为true)。终端只会影响到wait状态、sleep状态和join状态。被打断的线程会抛出InterruptedException。

Thread.interrupted()检查当前线程是否发生中断,返回boolean
synchronized在获锁的过程中是不能被中断的。

因此,正常运行的程序不去检测状态,就不会终止,而wait等阻塞方法会去检查并抛出异常。如果在正常运行的程序中添加while(!Thread.interrupted()) ,则同样可以在中断后离开代码体

控制线程停止的方法:在线程类中定义一个Boolean类型的属性,对于while()循环,将其Boolean类型的属性设为false即可。

2、join()方法:优先执行完当前线程

join()方法:表示先执行完该线程(即调用join方法的线程),再执行其他线程。

3、yield()方法:暂停当前正在执行的线程对象,并执行其他线程

四、线程同步(重点)

共享资源一定要设置好同步,防止其他未设置同步的方法操作共享资源,引发冲突,造成数据错误(重点)

(一)线程同步方法

JDK1.5之前,采用关键字synchronized和监视器机制来实现线程同步,JDK1.5之后,采用锁的机制实现线程同步。

1、 synchronized 修饰非静态方法,此时在这个方法体内的就有了 ”监视器“,它是所在类的对象.

                                                synchronized void func(){}

2、 synchronized 修饰静态方法,此时在这个方法体内的也有了 ”监视器“,它是所在类的对象.

                                                static synchronized void func(){}

3、 synchronized 修饰语句块,此时需要一个参数,而这个参数就作为“ 监视器 ”。

                                               synchronized(Object o){ }

监视器就是一个对象,不过他是有条件的,线程获取监视器才能执行synchronized修饰的方法或者代码。

java.util.concurrent.locks包提供了锁和等待条件的接口和类, 可用于替代JDK1.5之前的同步(synchronized)和监视器机制(主要是Object类的wait(), notify(), notifyAll()方法).

(二)锁的机制概念

1、所谓互斥锁, 指的是一次最多只能有一个线程持有的锁. 在jdk1.5之前, 我们通常使用synchronized机制控制多个线程对共享资源的访问. 

而现在, Lock提供了比synchronized机制更广泛的锁定操作。

2、Lock和synchronized机制的主要区别:

  • synchronized机制提供了对与每个对象相关的隐式监视器锁的访问, 并强制所有锁获取和释放均要出现在一个块结构中, 当获取了多个锁时, 它们必须以相反的顺序释放. synchronized机制对锁的释放是隐式的, 只要线程运行的代码超出了synchronized语句块范围, 锁就会被释放. 
  • Lock机制必须显式的调用Lock对象的unlock()方法才能释放锁, 这为获取锁和释放锁不出现在同一个块结构中, 以及以更自由的顺序释放锁提供了可能. 以下代码演示了在不同的块结构中获取和释放锁。

(三)线程同步相关概念

1、线程同步:线程可能和其他线程共享一些资源,比如:文件,数据,内存等。当多个线程同时读写共享资源时,就可能发生冲突,为了解决这个问题,引入了线程同步机制,线程同步的真实意思,其实是“排队”:几个线程之间要排队,依次对共享资源进行操作,而不是同时进行操作。保证数据的准确性和唯一性,实现线程安全。

2、线程安全:多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问,必须等该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。 

注意:对全局变量和静态变量操作在多线程模型中会引发线程安全问题。

3、并行:多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时。相当于两个人同时做事情。

4、并发:通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。相当于两个任务都请求运行,而处理器只能按受一个任务,就把这两个任务安排轮流进行,由于时间间隔较短,使人感觉两个任务都在运行。

注意:需要将事务和线程同步区分开,二者是不同层面的术语,应用场景不同。

多线程为了提高应用的执行效率而设置的,事务为保证一个操作的原子性而设置的

形象说明:多线程相当于多个人同时工作,在工作的过程中,大家可能都需要上厕所(厕所只有一个),这时就需要进行排队,每个人依次上厕所,第一个上厕所的人需要上锁,防止其他人进入,用完解锁,下一个人再用。这就是多线程和线程同步。

(四)使用synchronized实现线程同步

synchronized关键字的用法(是关键字,内置特性)(重点)

1、synchronized的用法(作用:同一时间只能一个线程执行synchronized修饰的代码或者方法)

synchronized代码块使用格式:synchronized(synObject){  //程序代码  }

注意:synObject可以是this,代表获取当前对象的锁,也可以是类中的一个属性,代表获取该属性的锁。

synchronized方法:就是将synchronized放在方法声明中,表示整个方法是同步方法。

例如:public   synchronized   void   add(String    str){  //方法体   }

注意:每个类也会有一个锁(类锁),它可以用来控制对static数据成员的并发访问。

(五)通过lock实现线程同步

1、Lock接口的用法(ReentrantLock是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。)

问题:当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。

但是采用synchronized关键字来实现同步的话,就会导致一个问题:

  如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。

  因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到。

注意:Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。

public interface Lock {  
    void lock();  
    void lockInterruptibly() throws InterruptedException;  
    boolean tryLock();  
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;  
    void unlock();  
    Condition newCondition();  
}  

其中lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用来获取锁的。unLock()方法是用来释放锁的。

<1>.lock():如果锁已被其他线程获取,则进行等待。采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。

<2>.tryLock():有返回值,表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。

tryLock(long time, TimeUnit unit):  和tryLock()方法是类似的,区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。

<3>.lockInterruptibly()

获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用thread B.interrupt()方法能够中断线程B的等待过程。

注意,当一个线程获取了锁之后,是不会被interrupt()方法中断的。单独调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。

  因此当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的。

(六)总结:

      1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;

  2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

  3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;

  4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。

  5)Lock可以提高多个线程进行读操作的效率。

  在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。

(七)死锁示例

类似案例:五个科学家吃放,互相占用筷子,最后饿死。

对于多线程,最基本的原则是:线程安全大于性能,一定要保证线程安全。

public class DealThread implements Runnable{
	public int flag = 1;
	static static Object o1 = new Object(),o2 = new Object();   //注意这里是静态的对象
	@Override
	public void run() {
		System.out.println("flag="+flag);
		if (flag == 1) {
			synchronized (o1) {
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				synchronized (o2) {
					System.out.println("1");
				}
			}
		}
		if (flag == 0) {
			synchronized (o2) {
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				synchronized (o1) {
					System.out.println("0");
				}
			}
		}
			
	}
	public static void main(String[] args) {
		DealThread d1 = new DealThread();
		DealThread d2 = new DealThread();
		d1.flag = 1;
		d2.flag = 0;
		Thread t1 = new Thread(d1);
		Thread t2 = new Thread(d2);
		t1.start();
		t2.start();
	}
	
}//注意这里是静态的对象
@Override
	public void run() {
		System.out.println("flag="+flag);
		if (flag == 1) {
			synchronized (o1) {
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				synchronized (o2) {
					System.out.println("1");
				}
			}
		}
		if (flag == 0) {
			synchronized (o2) {
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				synchronized (o1) {
					System.out.println("0");
				}
			}
		}			
	}
	public static void main(String[] args) {
		DealThread d1 = new DealThread();
		DealThread d2 = new DealThread();
		d1.flag = 1;
		d2.flag = 0;
		Thread t1 = new Thread(d1);
		Thread t2 = new Thread(d2);
		t1.start();
		t2.start();
	}	
}

参考文档:http://blog.csdn.net/g893465244/article/details/52538598

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一位远方的诗人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值