浅谈Java中对线程的理解

来源:互联网 发布:淘宝acg hires 编辑:程序博客网 时间:2024/05/08 05:25

  谈这个之前首先得区分一下什么是线程什么是进程?

 进程:正在运行中的程序。
 线程:就是进程当中的执行单元或者执行情景或者执行路径。负责进程中程序执行的控制单元。 当一个进程当中,线程有多个时,就是多线程,我们可以把这个应用程序称之为多线程应用程序。 其实java程序就是一个多线程程序。在执行main函数的同时,垃圾回收器也在回收堆内存当中的垃圾, 所以执行main函数的线程和执行垃圾回收器的线程是同时进行的,这就是多线程。jvm在启动线程的时候,每个线程都有自己的执行内容,其中一个负责执行main函数的内容,那么这个线程我们就称之为主线程。负责垃圾回收器运行的线程我们称之为垃圾回收线程。

 为什么要启动多线程呢?
 因为有多部分的代码需要同时执行,而且每一个线程都有自己要执行的内容,这个内容我们称之为线程的任务。简单说:启动线程就是为了执行任务,当任务有多个需要同时执行的时候,就是多线程。

如何去创建线程呢?
 java既然需要调用底层来实现进程的建立和线程的创建,那么java应该有对外提供一个 描述线程的对象的类,来方便程序员对线程的操作。 java的java.lang包当中,去找线程的核心类Thread。

通过查阅thread描述可以发现:
 创建线程有两种方式:
 方式一: 继承thread类,然后重写run()方法。
 步骤:
  1.定义类,继承thread类。
  2.重写thread类当中的run方法。
  3.创建thread类的子类对象,也就是创建了线程对象。
  4.调用线程对象的start方法开启线程。

调用run方法没有执行新创建的线程,还是一个线程在执行,主线程创建了
两个线程对象之后,主线程指向student中的run方法,其实在做操作的一值是主线程。
线程只创建不行,还需要运行才会出现多条执行路径。
运行线程必须要使用start方法。
  start方法有两个作用:
    1.开启了线程,让线程可以运行。
    2.让jvm虚拟机调用了run方法。

调用run方法和调用start方法的区别?
     调用run方法,仅仅是一般对象在调用对象当中的方法,并没有开启线程,
     还是由主线程来执行run方法的内容的。
     调用start方法就开启了新的线程(有了一条新的执行路径),
     这个线程就去执行run方法。
多线程的运行状态:
1.新建状态(New):
  当用new操作符创建一个线程时, 例如new Thread(r),线程还没有开始运行,
  此时线程处在新建状态。 当一个线程处于新生状态时,程序还没有开始运行线程中的代码.

2.就绪状态(Runnable)
   一个新创建的线程并不自动开始运行,要执行线程,必须调用线程的start()方法。
   当线程对象调用start()方法即启动了线程,start()方法创建线程运行的系统资源,
   并调度线程运行run()方法。当start()方法返回后,线程就处于就绪状态。
   处于就绪状态的线程并不一定立即运行run()方法,线程还必须同其他线程竞争
CPU时间,只有获得CPU时间才可以运行线程。因为在单CPU的计算机系统中,不可能同时
运行多个线程,一个时刻仅有一个线程处于运行状态。因此此时可能有多个线程处于就
绪状态。对多个处于就绪状态的线程是由Java运行时系统的线程调度程序(thread
scheduler)来调度的。

3.运行状态(Running)
        当线程获得CPU时间后,它才进入运行状态,真正开始执行run()方法.

4. 阻塞状态(Blocked)
        线程运行过程中,可能由于各种原因进入阻塞状态:
        1>线程通过调用sleep方法进入睡眠状态;
        2>线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会
返回到它的调用者;
        3>线程试图得到一个锁,而该锁正被其他线程持有;
        4>线程在等待某个触发条件;
 所谓阻塞状态是正在运行的线程没有运行结束,暂时让出CPU,这时其他处于就绪状态
的线程就可以获得CPU时间,进入运行状态。

 5. 死亡状态(Dead)
        有两个原因会导致线程死亡:
        1) run方法正常退出而自然死亡,
        2) 一个未捕获的异常终止了run方法而使线程猝死。
        为了确定线程在当前是否存活着(就是要么是可运行的,要么是被阻塞了),
需要使用isAlive方法。如果是可运行或被阻塞,这个方法返回true; 如果线程仍旧是
new状态且不是可运行的, 或者线程死亡了,则返回false.

 线程安全问题:
 
  安全问题产生的原因:
 * 1.多个线程在操作共享数据。
 * 2.操作共享数据的代码有多条.
 *
 * 分析线程是否会有安全问题呢?
 * 依据:线程任务当中有没有共享数据,该数据是否被多条语句操作。
 *
 * 产生安全问题的解决方案:
 * 只要保证一个线程在执行多条操作共享数据的语句时,其他线程不能参与运算即可。
 * 当该线程执行完之后,其他线程再来执行这个语句。
 *
 * 代码体现:
 * java给我们提供了具体的代码:就是同步代码块
 *   格式:synchronized(对象){   //这个对象可以是任意的对象
 *            //需要被同步执行的语句
 *       }
 *
 * 同步的原理:其实就是将需要同步的代码进行了封装,然后再封装的代码块之上加了一把锁。
 *
 * 同步的好处:解决了多线程的安全问题。
 * 同步的弊端:会降低性能。
 *
 * 一种现象,出现了多线程安全性问题,为了解决,加了同步,发现问题依旧,
 * 产生这个现象的原因是什么呢?
 *
 *同步的前提:
 *1.必须要保证同步中有多个线程,因为同步中只有一个线程这个同步是没有任何意义的。
 *2.必须要保证多线程在同步中使用的是同一把锁。
 *
 *必须要保证多线程的锁是同一个,这样才能使多线程同步。

怎么让函数具备同步性呢?
直接用同步关键字作为修饰符,修饰函数即可,这时函数就具备了同步性。
 这个函数就是同步函数,他是同步的另一种体现形式。
 这种形式写法简单。

 通过两个线程来验证一下同步函数的锁是什么?
 这两个线程都是在卖票,一个在同步代码块中卖票,一个在同步函数中卖票。
 如果两个线程用到是同一把锁,就会同步,就不会出现卖错票的情况。
 
 同步函数使用的锁是什么呢?
 函数被对像调用,代表调用函数对象的引用就是this.
 那也就是说同步函数使用的锁就是this。
 
  同步代码块和同步函数之间有什么区别呢?
  1.同步函数比同步代码块写法简单。
  2.同步函数使用的锁是this,同步代码块使用的锁是任意的对象。
 
  建议开发时使用同步代码块,因为锁可以是任意的,尤其是我们需要不同的锁的时候,
  同步代码块才能解决这个问题。

静态同步函数的锁是什么?
 静态随着类的加载而加载,而这时内存中存储的对象至少有一个,也就是该类的字节码文件对象。
 这个对象的表现形式:类名.class,他表示字节码文件对象

[关于单例模式的分析]
//饿汉式
class Single{
    private Single(){}
    private static final Single s = new Single();
    public static Single getInstance(){
        return s;
    }
}
/**如果饿汉式在多线程当中会有线程不安全么?
 * 答:不会,因为操作共享数据的语句只有一条,所以不会导致线程不安全现象。
 */


//懒汉式:单例模式的延迟加载
class Single2{
    private static Single2 s = null;
    private Single2(){}
    
    public  static Single2 getInstance(){
        if (s==null) {
            synchronized (Single2.class) {
                if (s==null) {
                    s = new Single2();
                }
            }
        }
        return s;
    }
}
/**分析懒汉式线程不安全原因:
 * 有共享数据s,然后共享数据操作语句有多条,判断,赋值
 * 怎么解决这个问题呢?
 * 上锁。
 * 懒汉式在多线程处理时会出现线程不安全的问题,为了解决加了同步,
 * 虽然安全问题就解决了,但是性能降低了。
 *
 * 有没有办法在安全问题解决的同时又能提高性能呢?
 * 方法是有的,使用同步代码块形式,
 * 通过双重判断来完成这个过程。
 * */

同步的另一个弊端:死锁
 最常见的死锁的情况:
 同步嵌套,同步中还有同步,然后两个同步用的不是一把锁。
 
 尽量避免同步嵌套的情况

死锁的四个必要条件
 1.互斥条件:资源不能被共享,只能由一个线程来使用。
 2.请求与保持条件:已经得到了资源的进程,还可以申请新的资源。
 3.非剥夺条件:已经分配的资源不能在相应的线程中前行的剥夺。
 4.循环等待条件:系统中若干个线程组成了环路,该环路中每一个线程都在等待着相邻的线程占据的资源。


[线程间通信]
 等待唤醒机制:
 涉及到3个方法:
  wait():等待,将正在执行的线程释放了其执行资格和执行权力,并且将它存储到线程池中。
  (何为线程池:运行当中会出现很多正在被冻结的线程,都会储存到线程池当中,线程池其实就是容器,
  用来存储线程对象的,暂时不用的线程也要留着)。
 
  notify():唤醒,唤醒了线程池中被wait了的线程,注意一次只能唤醒一个,而且唤醒的线程
  对象时任意的。
 
  notifyAll():唤醒全部,他可以将线程池中所有被wait的线程都唤醒。
 
  唤醒就是让线程池中的线程具备执行资格。
 
  这些方法只有使用在同步中才能有效。
  这些方法在使用时必需要表明所属的锁,这样我们才可以明确方法操作的到底是哪个锁上的线程。
 
  为什么这些操作线程的方法要定义在object类当中。
  因为这些方法在使用时必须要表明所属的锁,而锁可以是任意对象,能被任意对象调用的方法
  一定定义在object类当中。

 等待唤醒机制的优化:代码的重新整理
  代码的优化:
  把同步的代码的操作内容定义在资源中,比较好,为什么呢?
  因为更符合面向对象的思想。
 
  把类中的方法设置为synchronized修饰符修饰,那么他的对象就是线程安全的对象。



0 0
原创粉丝点击