线程知识点(理解与总结)

线程知识点(理解与总结)

进程:指一个内存中运行的应用程序。例如运行QQ那么它就是一个进程,而且一个应用程序可以同时运行多个进程

线程:线程是进程中的一个执行单元,就比如用360我们可以让它一边杀毒一边清理垃圾,那么360它就是一个进程,那么杀毒和清理垃圾就是进程下的两个线程
注:一个程序运行后至少有一个进程,一个进程中可以包含多个线程

Thread类

构造方法:
public Thread() :分配一个新的线程对象。

public Thread(String name) :分配一个指定名字的新的线程对象。

public Thread(Runnable target) :分配一个带有指定目标新的线程对象。

public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字。

常用方法
public String getName() :获取当前线程名称。

public void start() :导致此线程开始执行; Java虚拟机调用此线程的run方法。

public void run() :此线程要执行的任务在此处定义代码。

public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停

public static Thread currentThread() :返回对当前正在执行的线程对象的引用。

创建线程的方式总共有两种:
1.是继承Thread类方式
2.是实现Runnable接口

创建线程的方式一:

  1. 定义Thread类子类,并重写该类的run()方法,run()方法的方法体就代表了线程需要完成的任务,
  2. 创建Thread子类的实例3. 调用Thread的start()方法来启动该线程

代码如下:

public class Test {
	public static void main(String[] args) {
		MyThread mt = new MyThread();
		Thread t1 = new Thread(mt);		
		t1.start();	
	}
}
class MyThread extends Thread{
	@Override
	public void run(){
	System.out.println("第一种方式创建线程");	
	}
}

创建线程的方式二:

  1. 定义Runnable接口的实现类,并重写该接口的run()方法,run()方法的方法体代表线程完成的任务
  2. 创建Runnable实现类的实例。
  3. 调用Thread的start()方法来启动线程。

代码如下:

public class Test {
	public static void main(String[] args) {
		MyRunnable mr = new MyRunnable();
		Thread t1 = new Thread(mr);
		t1.start();
	}
}
class MyRunnable implements Runnable{
	@Override
	public void run(){
	System.out.println("第二种方式创建线程");	
	}
}

Thread和Runnable的区别

因为类都是单继承的,如果一个类继承Thread,就不可以继承其他类了。但是如果实现了Runnable接口的话,则很容易的实现资源共享,避免了java中的单继承的局限性,所以Runnable比Thread更有优势

用匿名内部类创建线程

使用匿名内部类的方式实现Runnable接口,重写Runnable接口中的run方法:

代码如下:

public class Test {
	public static void main(String[] args) {
		new Thread(new Runnable(){
			@Override
			public void run() {
				System.out.println("匿名内部类创建线程");
			}
		}).start();
	}
}

线程安全问题的概述

如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
那么什么是线程不安全的呢?我们由一段代码看一下:
这里我想打印的是1-100之间的整数:

public class Test {
  public static void main(String[] args) {
   MyRunnable r = new MyRunnable();
   Thread t1 = new Thread(r);
   Thread t2 = new Thread(r);
   Thread t3 = new Thread(r);
   t1.start();
   t2.start();
   t3.start(); 
  }
}
class MyRunnable implements Runnable{
  private int sum = 100;
  public void run(){
   while(true){
    if(sum>0){
     try {
      Thread.sleep(10);
     catch (InterruptedException e) {
      e.printStackTrace();
     }
     System.out.println(sum);
     sum--;
    }
   }
  }
}

运行结果出现了很多重复的,甚至还有-1,结果和预期是不一样的,这就是线程不安全情况。那么为什么会出现这种情况呢?是因为这三个线程在执行过程中不断抢夺CPU的执行权,当某一个线程运行到Thread.sleep(10)的时候处于睡眠状态,那么CPU的执行权交给了另外两个线程以此类推,三个线程都执行到了这里,这时代码就不是一条判断一条输出了,当睡眠结束后三条线程面临的都是一条输出语句一个sum–不再判断sum的值,若最后判断sum的值为1,最后sum的值将被–三次,所以才会导致最终的结果出现0和-1的情况(最终sum的值为-2),这里的Thread.sleep()其实是为了增加线程安全问题出现的概率。

线程同步

当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。 要解决上述多线程并发访问一个资源的安全性问题,Java中提供了同步机制(synchronized)来解决。

同步代码块

无论是否失去CPU的执行权,让其他线程只能处于等待状态

格式:

 synchronized(同步锁){ 
	//需要同步操作的代码
}

使用同步代码块解决代码(运行结果准确无误的输出1-100之间的整数):

public class Test {
  public static void main(String[] args) {
   MyRunnable r = new MyRunnable();
   Thread t1 = new Thread(r);
   Thread t2 = new Thread(r);
   Thread t3 = new Thread(r);
   t1.start();
   t2.start();
  t3.start();
  }
}
class MyRunnable implements Runnable{
  private int sum = 100;
  Object obj = new Object();
  public void run(){
   while(true){
    synchronized(obj){
     if (sum > 0) {
      try {
       Thread.sleep(10);
      } catch (InterruptedException e) {
       e.printStackTrace();
      }
      System.out.println(sum);
      sum--;
     }
    }
   }
  }
}

修饰的方法,就叫做同步方法,保证某一线程执行该方法的时候,其他线程只能在方法外等着。

使用同步方法代码如下:

public class Test {
 public static void main(String[] args) {
   MyRunnable r = new MyRunnable();
   Thread t1 = new Thread(r);
   Thread t2 = new Thread(r);
   Thread t3 = new Thread(r);
   t1.start();
   t2.start();
   t3.start();
 }
}
class MyRunnable implements Runnable {
  private int sum = 100;
  Object obj = new Object();
  public void run() {
   while (true) {
    show();
   }
  }
 public synchronized void show() {
  if (sum > 0) {
   try {
    Thread.sleep(10);
   } catch (InterruptedException e) {
    e.printStackTrace();
   }
   System.out.println(sum);
   sum--;
  }
 }
}

Lock锁

锁定操作比synchronized代码块和synchronized方法更广泛, 同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。

public void lock() :加同步锁。
public void unlock() :释放同步锁。

使用如下:

public class Test {
 public static void main(String[] args) {
   MyRunnable r = new MyRunnable();
   Thread t1 = new Thread(r);
   Thread t2 = new Thread(r);
   Thread t3 = new Thread(r);
   t1.start();
   t2.start();
   t3.start();
 }
}
class MyRunnable implements Runnable {
  private int sum = 100;
  Lock l = new ReentrantLock();
  public void run() {
   while (true) {
    l.lock();
    if (sum > 0) {
     try {
      Thread.sleep(10);
      System.out.println(sum);
      sum--;
     } catch (InterruptedException e) {
      e.printStackTrace();
     } finally {
      l.unlock();
    }
   }
  }
 }
}

计时等待状态

为一个线程增加了限时等待的状态。在我们写1-100之间整数的案例中,为了增加线程安全问题出现的概率也为了减缓执行速度,我们在run方法中添加了sleep语句,这样就强制当前正在执行的线程休眠(暂停执行)。 其实当我们调用了sleep方法之后,当前执行的线程就进入到“休眠状态”,到时间自动苏醒,其实就是所谓的计时等待状态。

锁阻塞状态

一个线程正在等待一个监视器锁(锁对象)的线程处于的状态,也就是为获取锁对象的状态比如,线程A与线程B代码中使用同一锁,如果线程A获取到锁,那么线程B就进入到锁阻塞状态。

无限等待状态

一个线程处于正在无限期等待另一个线程执行一个特别的(唤醒)动作的这一状态,也就是调用wait()方法进入无限等待状态,直到调用notify方法,释放锁对象。
下面通过一段代码理解一下:

public class ThreadDemo {
 private static boolean isfinished;
 private static Object obj = new Object();
 public static void main(String[] args) {
  new Thread() {
   public void run() {
    System.out.println("开始下载图片");
    for (int i = 0; i <= 100; i++) {
     try {
      Thread.sleep(20);
     } catch (InterruptedException e) {
      e.printStackTrace();
     }
     System.out.println("down:图片已下载:" + i + "%");
    }
    System.out.println("图片下载完毕");
    isfinished = true;
    synchronized (obj) {
     obj.notify();
    }
    System.out.println("开始下载附件");
    for (int i = 0; i <= 100; i++) {
     try {
      Thread.sleep(20);
     } catch (InterruptedException e) {
      e.printStackTrace();
     }
     System.out.println("附件:图片已下载:" + i + "%");
    }
    System.out.println("附件下载完毕");
   }
  }.start();
  new Thread() {
   public void run() {
    if (!isfinished) {
     System.out.println("图片还没下载完成");
    }
    System.out.println("开始显示图片");
    try {
     synchronized (obj) {
      obj.wait();
     }
    } catch (InterruptedException e) {
     e.printStackTrace();
    }
    System.out.println("图片显示完毕");
   }
  }.start();
 }
}

通过上述案例我们会发现,一个调用了某个对象的wait 方法的线程会等待另一个线程调用此对象的 notify()方法。其实无限等待状态并不是一个线程的操作,它体现的是多个线程间的通信,可以理解为多个线程之间的协作关系,多个线程会争取锁,同时相互之间又存在协作关系。当多个线程协作时,比如A,B线程,如果A线程在可运行状态中调用了wait()方法那么A线程就进入了无限等待状态,同时失去了同步锁。假如这个时候B线程获取到了同步锁,在运行状态中调用了notify()方法,那么就会将无限等待的A线程唤醒。注意是唤醒,如果获取到锁对象,那么A线程唤醒后就进入可运行状态;如果没有获取锁对象,那么就进入到锁阻塞状态。

等待唤醒机制概述

这是多个线程间的一种协作机制。就是在一个线程进行了规定操作后,就进入等待状态(wait()),等待其他线程执行完他们的指定代码过后再将其唤醒(notify());在有多个线程进行等待时,如果需要,可以使用 notifyAll()来唤醒所有的等待线程。wait/notify 就是线程间的一种协作机制。

等待唤醒中的方法

  1. wait:线程不再活动,不再参与调度,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态即是无限等待状态。它还要等着别的线程执行一个特别的动作,也即是通知(notify)在这个对象上等待的线程等待中释放出来,重新进入到调度队列中
  2. notify:把一个等待状态中的一个线程释放出来
  3. notifyAll:释放所有等待状态的线程。

调用wait和notify方法需要注意的细节

  1. wait方法与notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。
  2. wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。
  3. wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这2个方法。

线程池思想概述

我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。 那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁而是可以继续执行其他的任务? 在Java中可以通过线程池来达到这样的效果。

线程池概念

其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。利用线程池能够带来的好处:

  1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。

线程池的使用

线程池接口是 ExecutorService 。Executors类中有个创建线程池的方法如下:public static ExecutorService newFixedThreadPool(int nThreads) :返回线程池对象。获取到了一个线程池ExecutorService 对象,然后定义一个使用线程池对象的方法如下:public Future submit(Runnable task) :获取线程池中的某一个线程对象

使用线程池中线程对象的步骤:

  1. 创建线程池对象。
  2. 创建Runnable接口子类对象。(task)
  3. 提交Runnable接口子类对象。(take task)
  4. 关闭线程池(一般不做)。

实现代码:

public class Test {
 public static void main(String[] args) {
  // 1、生产一个指定线程数量的线程池
  ExecutorService es = Executors.newFixedThreadPool(2);
  // 2、调用submit方法,传递线程任务(实现类),开启线程执行run方法
  es.submit(new RunnableImpl());// Thread[pool-1-thread-2,5,main]执行
  // 线程池会一直开启, 使用完了线程,会自动把线程会还给线程池,线程可以继续使用
  es.submit(new RunnableImpl());// Thread[pool-1-thread-1,5,main]执行
  es.submit(new RunnableImpl());// Thread[pool-1-thread-2,5,main]执行
  // 调用ExecutorService中的方法shutdown销毁线程池(不建议执行)
  es.shutdown();
//es.submit(new RunnableImpl());// 抛异常,线程池都没有了,就不能获取线程了
 }
}
// 创建一个类,实现Runnable接口,重写run方法,设置线程任务
class RunnableImpl implements Runnable {
 @Override
 public void run() {
  System.out.println(Thread.currentThread()+ "执行");
 }
}

你可能感兴趣的:(线程,Java基础,线程,知识点总结,深入学习理解)