Java线程

创建线程的方法

1、继承Thread类

 格式如下:

public class 类名 extends Thread {
    // 重写run()方法 (不要重写其他方法,不然运行程序会出bug)
    @Override
	public void run() {
		// TODO Auto-generated method stub
	}
}

2、实现Runnable接口

 Runnable接口源码:

package java.lang;
/**
 * @author  Arthur van Hoff
 * @see     java.lang.Thread
 * @see     java.util.concurrent.Callable
 * @since   JDK1.0
 */
@FunctionalInterface	// 函数式接口(可以使用lambda)
public interface Runnable {
    public abstract void run();
}

 格式如下:

public class TestRunnable implements Runnable {
    // 必须重写run()方法
	@Override
	public void run() {
		// TODO Auto-generated method stub
	}
}

// 使用匿名内部内创建
public static void main(String[] args) {
    new Thread(new Runnable() {
        @Override
        public void run() {
            // 返回线程ID
            Thread.currentThread().getId()
            // 返回线程名称
			Thread.currentThread().getName();
            // 返回线程状态
            Thread.currentThread().getState()
            // 返回线程的优先级 (默认优先级:5  最小优先级:1  最大优先级:10)
            Thread.currentThread().getPriority()
		}
	}, "线程名");
}

// 使用lambda实现
public static void main(String[] args) {
	new Thread(()->{
		// 方法体
	}, "线程名");
}

线程优先级

  MAX_PRIORITY // 最优先级 10
  MIN_PRIORITY // 最优先级 1
  NORM_PRIORITY // 默认优先级 5

threadName.setPriority(Thread.MAX_PRIORITY);	// 设置最大优先级  10
threadName.setPriority(Thread.MIN_PRIORITY);	// 设置最小优先级  1
threadName.setPriority(Thread.MIN_PRIORITY);	// 设置默认优先级  5

线程状态

  1. 新建状态(New)
  2. 就绪状态(Runnable)
  3. 运行状态(Running)
  4. 阻塞状态(Blocked)
  5. 死亡状态(Dead)

 线程状态转换:

image

包含了等待状态的线程状态转换图:

image

线程阻塞的方法:sleep()join()

sleep() 方法

  让线程进入休眠状态。
  sleep(毫秒数):使当前正在执行的线程以指定的毫秒数暂停。(设置线程休眠必须抛异常)

public static void main(String[] args) {
	new Thread(new Runnable() {
		@Override
		public void run() {
			try {
				for (int i = 0; i < 15; i++) {
					System.out.println(Thread.currentThread().getName() + "第:" + i + "次");
					// sleep(毫秒):线程休眠
					Thread.sleep(1000); // 设置当前线程休眠1秒
				}
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}, "线程1").start();// start():开始执行此线程; Java虚拟机调用此线程的run方法。
}

join() 方法

  join():等待这个线程死亡。

     主线程中有多个子线程,等子线程执行完后,在结束主线程。

     通常用在main()方法内。(使用join()方法必须抛异常)

  主线程和一个A线程,如果A线程调用了join()方法,就会将A线程何如到主线程中,首先执行主线程,主线程执行过程中会启动A线程,A线程在运行过程中,主线程进入阻塞状态,等到A线程运行完毕,在执行主线程。

例:

public static void main(String[] args) {
	System.out.println("Main start");
	Thread threadOne = new Thread(new Runnable() {
		@Override
		public void run() {
			for (int i = 0; i < 5; i++) {
				System.out.println(Thread.currentThread().getName() + "第:" + i + "次");
			}
			
		}
	}, "线程1");
	
	Thread threadTwo = new Thread(()->{
		for (int i = 0; i < 5; i++) {
			System.out.println(Thread.currentThread().getName() + "第:" + i + "次");
		}
	}, "线程2");
	
	threadOne.start();	// 执行线程1
	threadTwo.start();	// 执行线程2
	
	try {
		threadOne.join();
		threadTwo.join();
	} catch (InterruptedException e) {
		e.printStackTrace();
	}
	
	System.out.println("Main end");
}

线程Lambda表达式

 Runnable接口是一个函数式接口,可以为Rannable接口编写匿名函数

public class TestLambda {
	public static void main(String[] args) {
		for(int i=0;i<4;i++) {
			new Thread(()->{
				for(int j=1;j<=10;j++) {
					System.out.println(Thread.currentThread().getName()+":::"+j);
				}
			}, "递增线程"+(i+1)).start();
		}
	}
}

如何停止一个线程

1. stop():强制停止线程,工作中不要使用

2. 使用定时器去停止正在执行的线程,定时器是一个监听者,监听线程。

 定时器:设定一个周期,按照指定的周期重复的执行任务。

 定时器有一个定时任务(实现了Runnable接口)。

import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

/**
 * 定时器每隔1秒钟打印一次系统当前时间
 */
public class TestTimerr {
	public static void main(String[] args) {
		Timer timer = new Timer();
		// 参数1:定时器里面的定时任务;
        // 参数2:delay延迟多长时间执行
		/*timer.schedule(new TimerTask() {
			@Override
			public void run() {
				System.out.println(new Date());
			}
		}, 1000); */
        
		// Java的定时器:指定一个周期重复执行
		// 参数1:定时器里面的定时任务
		// 参数2:delay延迟多长时间执行(此时表示当前时间之后的1秒钟开始执行定时任务)
		// 参数3:每次任务执行的间隔周期(此时表示每个2秒执行一次定时任务)
		timer.schedule(new TimerTask() {
			@Override
			public void run() {
				System.out.println(new Date());
			}
		}, 1000,2000);
	}
}

  场景:main线程中启动一个ThreadA,同时开启一个定时任务去监听ThreadA,定时任务每间隔5秒钟扫描当前工程有没有stop文件,如果有将ThreadA线程停止掉。

import java.io.File;
import java.util.Timer;
import java.util.TimerTask;

public class TestStop {
	public static void main(String[] args) {
		StopTask stop = new StopTask(true);
		Thread stopThread = new Thread(stop);
		stopThread.start();
		// 创建定时器,定时器包含一个定时任务,每个5秒钟检查当前工程下面有没有stop文件,如果有停止线程
		Timer timer = new Timer();
        // schedule()方法调度定时任务的执行
		timer.schedule(new TimerTask() {
			@Override
			public void run() {
				File file = new File("stop");
				// 条件成立:表示stop文件存在,停止线程
				if(file.exists()) {
					stop.setFlag(false);
					// StopTask任务停止,立马删除stop文件
					file.delete();
					// 取消定时任务
					timer.cancel();
				}
			}
		}, 1000,5000);
	}
}
class StopTask implements Runnable{
	private boolean flag ;

	public void setFlag(boolean flag) {
		this.flag = flag;
	}
	public StopTask(boolean flag){
		this.flag = flag;
	}

	@Override
	public void run() {
		long index=0;
		for(;flag;) {
			index++;
			if(index%500000000==0) {
				System.out.println(Thread.currentThread().getName()+":::"+index);
			}
		}
	}	
}

image

守护线程

线程:执行线程(用户线程)、守护线程(守护执行线程的,一旦执行线程结束了,守护线程也会结束)

通常JVMGC线程就是一个守护线程

// 设置t1线程为守护线程
t1.setDaemon(true);

同步

线程安全和非线程安全

  非线程安全:多个线程同一时刻访问一个共享资源,造成共享资源的数据出现脏数据,不安全(数组下标越界)

  线程安全:需要对共享资源上一把锁,确保同一时刻最多只能有一个线程访问共享资源,其他线程在外面等待,不会出现脏数据。

synchronized 关键字

  同步:对共享资源进行同步,确保同一时刻最多只能有一个线程访问共享资源

  共享资源同步特征:确保共享资源在每个线程使用过程中数据的有效性和一致性,避免脏数据

  同步机制:为共享资源上锁

  synchronize就是为共享资源上锁,确保同一时刻最多只能有一个线程访问共享资源

小结

  • 工作中共享资源必须加锁

  • 为了提供程序在内存中运行的效率,尽量不要锁住整个方法,而是锁代码块

  • 那些代码块需要上锁?

    first:某个共享资源的数据经常会发生改变

    second:被多个线程使用

  • 使用synchronize修饰某个块代码,叫做同步块

  • 被synchronize修饰的代码叫做临界区(临界区的代码块最多只能有一个线程访问)

  • synchronize是一个JVM级别的互斥锁(排它锁)

    JVM帮你加锁和解锁(不是人为加锁和解锁)

    缺点:一旦出现异常可能无法解锁,因为不是你手工加锁

  • 同步块括号不能少,括号里面的对象表示你要锁住的共享资源,最好使用final修饰,因为不可改变的

  • synchronize括号里面的对象不支持基本类型

Lock 重入锁

 @SinceJDK1.5,重入锁可以进行加锁和解锁,比synchronize更加友好,一旦出现异常可以人工解锁

   特征:有一个公平机制,等待时间最长的线程优先进入共享资源

 创建格式如下:

//true:启动公平锁  false:非公平锁  默认false
private Lock lck = new ReentrantLock(true);

 使用Lock格式如下:

// 加锁
lck.lock();
try{
   
}finally{
    // 解锁
    lck.unlock();
}
// 好处:一旦try块出现异常执行finally,将锁解掉
// 解锁操作工作中一定要放在finally块中,出现异常立马解锁。
// 工作中如果使用Lock,一定先编写整体(try...finally块),再编写局部(try块里面的内容)

重入锁功能详解 https://www.cnblogs.com/takumicx/p/9338983.html

读写锁

  由读锁(如果你为多个方法加了读锁,操作多个读方法的线程可以同时进入临界区)和写锁(如果多个方法加上了写锁,最多只能有一个线程进入临界区)组成。

  :并行,一旦操作共享资源多个线程的读方法进入临界区,所有的操作写方法的线程必须在外面等待

  :串行,一旦某个线程进入了贡献在资源的写方法临界区,所有的读方法在外面等待,其他的操作写方法的线程也将在外面等待。

一般而言:只是访问共享资源的数据添加读锁,改变共享资源的数据添加写锁。

一旦某个读方法进入临界区,其他的读方法也可以进入,所有的写方法在临界区外面(锁池)等待

一旦某个写方法进入临界区,其他的写方法在临界区外等待,所有的读方法也在临界区外面等待

读写锁效率高于重入锁(Lock)

  wait() notify() notifyAll():必须配合synchronized或者Lock一起使用,如果没有synchronized或者Lock会在程序运行时抛出IllegalMonitorStateException。因为wait()不能多个线程同时进入,一旦多个线程同时进入wait()就会出现死锁的可能,JVM会抛出IllegalMonitorStateException。

  阻塞状态:wait()/notify()/notifyAll() sleep() join()

  死锁:所有的线程都进入了wait() 阻塞状态,无法唤醒对方

  正确情况:生产者唤醒消费者,消费者也能唤醒生产者

常见面试题:

  解释线程的死锁?工作中你遇到过线程死锁码?什么情况会发生线程死锁

  IllegalMonitorStateException是什么异常?工作中有没有遇到过该异常?如何避免该异常

  sleep()方法和wait()方法区别?

    sleep()方法不会释放共享资源的锁,wait()会释放共享资源的锁

Lock如何代替synchronized?

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 *  店员是一个共享资源 :可以服务生产者和消费者
 *  店员后面有一个餐台:只能放一个产品 -1 表示餐台空  其他表示餐台非空
 */
public class Clerk {

	/**
	 * 默认餐台为空:生产者开始生产,消费者等待
	 */
	private int product = -1;
	private Lock lck = new ReentrantLock(true);
	
	/**
	 * 锁的条件
	 * condition对象必须配合lck一起使用
	 */
	private Condition condition = lck.newCondition();
		// wait --->condition.await();
		// notify---->condition.signal();
		// notifyAll---->condition.signalAll();
	/**
	 * 生产者调用方法
	 * product:表示生产者生产的产品
	 */
	public void setProduct(int product)throws Exception{
		lck.lock();
		try {
			// 条件成立:餐台非空生产者等待
			while(this.product!=-1) {
				condition.await();
			}
			// 生产者开始生产产品
			this.product = product;
			// 生产完毕,通知(唤醒)消费者线程,拿走产品
			condition.signalAll();
			System.out.println("生产者线程生产了第"+this.product+"个产品...");
		} finally {
			lck.unlock();
		}
	}
	
	/**
	 * 消费者调用的方法
	 * @throws Exception
	 * IllegalMonitorStateException: wait()  notifyAll()必须配合synchronized一起使用
	 * 否则就会出现上面的异常
	 */
	public int  getProduct()throws Exception{
		lck.lock();
		try {
			//条件成立:表示餐台为空,消费者等待
			while(this.product==-1) {
				condition.await();
			}
			//消费者取走产品
			int tmp = this.product;
			//将餐台设置为空
			this.product =-1;
			//通知(唤醒)生产者开始生产
			condition.signalAll();
			System.out.println("消费者线程拿走了第"+tmp+"个产品...");
			return tmp;
		} finally {
			lck.unlock();
		}
	}
}

无界队列:LinkedBlockingQueue

  基于链表结构的阻塞式队列,也就是说它是建立在LinkedList基础上的

Blocking 阻塞:当集合中的元素已经放满了,还继续往里面添加就会阻塞

       当集合中的元素已经空了,还继续获取元素也会阻塞

特征:FIFO First-In-First-Out 先进先出

  无界限:容量可以是Integer的最大值 2^31-1

import java.util.concurrent.LinkedBlockingQueue;

public class TestLinkedBlockQueue {
	public static void main(String[] args) {
		// 队列最大容量为3
		LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<>(5);
		queue.add(1);
		queue.add(10);
		queue.add(100);
		// 超过最大容量,还在网里面添加元素就会抛出IllegalStateException: Queue full
		// queue.add(1000);
		try {
			// 队列元素已经到达上限(满了),还往里面放元素就会阻塞,InterruptedException在阻塞的过程中可能会发生中断异常
			queue.put(1000);
			System.out.println(queue.size());
			// 获取元素首部元素 结果:1
			Integer num = queue.peek();
			System.out.println(num);
			// size:4 仅仅获取元素没有删除(弹出)元素
			// System.out.println(queue.size());
			// 删除队列中的首部元素并返回删除(弹出)的结果
			// Integer num2 =queue.poll();
			// 队列中只有3个元素,循环执行poll()方法5次,既没有抛出异常,也没有阻塞,返回null
			// for(int i=0;i<5;i++) {
			// Integer num3=queue.poll();
			// System.out.println(num3);
			// }
			// System.out.println(queue.size());
			// take():获取队列首部元素,并将首部元素弹出(删除),一旦队列中没有元素了还在调用take()方法获取元素,程序就会阻塞
			for (int i = 0; i < 5; i++) {
				// 循环体执行第5次会阻塞
				Integer num6 = queue.take();
				System.out.println(num6);
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

往队列添加元素

  add:队列满了会抛出IllegalStateException异常

  offer:队列满了,拒绝处理(新元素无法添加到集合中),不会抛出异常也不会阻塞

     成功添加返回ture ,添加失败返回false

  put:队列满了程序进入阻塞状态,不会抛出异常

向队列获取元素

  peek:会获取首部元素,不会弹出(删除)元素

  poll:获取首部元素,并弹出(删除)元素

  take:获取首部元素,并弹出(删除)元素 , 如果队列为empty,程序进入阻塞

线程常见面试题

线程和进程有什么区别?

  线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。每个线程都拥有单独的栈内存用来存储本地数据。

start() 和 run() 方法有什么区别?

  start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程。

线程优先级的级别有哪些?

  高优先级10 、低优先级1、普通优先级5(默认)。

笔记过于简单,如需多多学习,详见:Java线程详解

原文地址:https://www.cnblogs.com/lyang-a/p/15077398.html