Java中的多线程基础知识
进程和线程概念
-
进程:是程序的一次执行过程,或是正在运行的一个程序,进程是一个动态的过程,存在生命周期(产生–>存在–>消亡),系统在运行时会为每一个进程分配不同的内存区域。
-
线程:一个进程可以进一步细分为线程,是一个程序内部的一条执行路径。线程是CPU调度和执行的单位,每个线程都拥有独立的运行栈和程序计数器,多个线程共享同一个进程中的方法区和堆。
-
一个Java程序就是一个进程,其中包含了至少三个线程,main()主线程,gc()垃圾回收线程,异常处理线程。
-
并行:多个CPU同时执行多个任务,一个进程内的多个线程可以被分配到不同的CPU资源同时进行(在同一时刻点)
-
并发:一个CPU(采用时间片)同时执行多个任务,只为一个含有多线程的进程分配仅有的CPU资源,这些线程会抢占时间片,线程之间会竞争CPU资源争取执行机会。在同一时刻点只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。
Java实现多线程的方法
1. 继承Thread类
- 创建一个继承于Thread类的子类
- 重写Thread类的run()
- 创建Thread类的子类的对象
- 通过此对象调用start() ①启动当前线程 ②调用当前线程的run方法()
//遍历100以内所有偶数
class MyThread extends Thread{
@Override
public void run() {
for(int i = 0;i<100;i++){
if(i % 2 ==0){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}
public class ThreadTest {
public static void main(String[] args) {
MyThread T1 = new MyThread();
T1.start();
//如下操作仍然是在main线程中执行的
for(int i = 0;i<100;i++){
if(i % 2 ==0){
System.out.println(i+"main");
}
}
MyThread T2 = new MyThread();
T2.start();
}
}
- Thread中的常用方法:
1. start():启动当前线程,调用当前线程的run()
2. run(): 通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
3. currentThread(): 静态方法,返回当前执行的线程
4. getName(): 获取当前线程的名字
5. setName(): 设置当前线程的名字
6. yield(): 释放当前cpu的执行权
7. join(): 在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态
8. stop(): 当执行此方法时,强制结束当前进程
9. sleep(long millitime): 让当前线程“睡眠"指定的millitime毫秒,在指定的millitime毫秒时间内,当前线程是阻塞状态
10. isAlive(): 判断当前线程是否存活
线程的优先级:
1. MAX_PRIORITY: 10 -->最高优先级
MIN_PRIORITY: 1 -->最低优先级
NORM_PRIORITY: 5 -->默认优先级
2. 获取当前线程的优先级: getPriority()
3. 设置当前线程优先级: setPriority() 高优先级的线程要抢占低优先级线程CPU的执行权,优先级高的有更高的概率比优先级低的先执行,但并不是一定比低优先级的线程先执行
2. 实现Runnable接口
- 创建一个实现了Runnable接口的类
- 实现类去实现Runnable中的抽象方法run()
- 创建实现类的对象
- 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
- 通过Thread类的对象调用start()
class MyThread1 implements Runnable{
@Override
public void run() {
for(int i = 0;i<100;i++){
if(i % 2 == 0){
System.out.println(i);
}
}
}
}
public class ThreadTest1 {
public static void main(String[] args) {
MyThread1 t1 = new MyThread1();
Thread T1 = new Thread(t1);
T1.start();
}
}
比较这两种创建线程的方式:
1. 实现接口的方式没有类的单继承性的局限性
2. 实现的方式更适合来处理多个线程有共享数据的情况
3. Thread类也是实现的Runnable接口
4. 都需要重写run()
3. 实现Callable接口
- 需要借助FutureTask类
- Future接口可以对具体Runnable,Callable任务的执行结果进行取消,查询是否完成,获取结果等。
- FutureTask是Future接口的唯一的实现类
- FutureTask同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
//1 创建一个实现Callable的实现类
class NumThread implements Callable{
//2 实现call()方法,将此线程需要执行的操作声明在call()中
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 0;i<=100;i++){
if(i%2 == 0){
System.out.println(i);
sum+=i;
}
}
return sum;
}
}
public class ThreadNew {
public static void main(String[] args) {
//3 创建Callable接口实现类的对象
NumThread numThread = new NumThread();
//4 将此Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象
FutureTask futureTask = new FutureTask(numThread);
//5 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
new Thread(futureTask).start();
try {
//6 获取Callable中的call()返回值
//get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值
Object sum = futureTask.get();
System.out.println("总和为"+sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
实现Callable()接口创建多线程相比实现Runable有哪些不同:
1. call()方法有返回值
2. call()可以抛出异常
3. Callable支持泛型
4. 使用线程池
-
当我们需要经常创建和销毁线程、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。如果提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中,就可以避免频繁创建销毁、实现重复利用。
-
好处:降低资源消耗,提高性能,便于线程管理。
-
主要参数:
1. corePoolSize:该线程池中核心线程数的最大值(核心线程:线程池新建线程的时候,如果当前线程总数小于corePoolSize,新建的是核心线程,如果超过corePoolSize,新建的是非核心线程,核心线程默认情况下会一直存活在线程池中)
2. maximumPoolSize:线程池最大大小,线程池所允许的最大线程(核心线程+非核心线程)个数。
3. keepAliveTime:非核心线程存活保持时间,当非核心线程存活的时间大于这个参数设置的时间则这个线程会被销毁
4. workQueue:该线程池中的任务队列,Runnable任务可以储存在任务队列中等待被执行。
5. threadFactory:用于创建新线程。
6. handler:用于抛出异常。 -
线程池的处理流程
通过源码中的注释可以简单的了解到线程池的处理流程有三步:
1. 当有任务提交时,如果运行的线程数少于corePoolSize,则会创建一个核心线程执行此任务
2. 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列,如果队列也满了,而且正在运行的线程数量小于 maximumPoolSize,则创建非核心线程执行这个任务;
3. 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,线程池就会抛出异常 -
四种常用的线程池
- newCachedThreadPool:可缓存线程池,用来创建一个可以无限扩大的线程池,适用于负载较轻的场景,执行短期异步任务。
corePoolSize:核心线程数为0,maximumPoolSize:线程池最大大小为Integer.MAX_VALUE(2147483647),keepAliveTime:非核心线程存活保持时间为60s,workQueue:该线程池中的任务队列为SynchronousQueue
- newFixedThreadPool:创建一个固定大小的线程池,实际线程数量永远不会变化,适用于负载较重的场景,对当前线程数量进行限制。
corePoolSize:核心线程数为nThread(自定义),maximumPoolSize:线程池最大大小为nThread(自定义),keepAliveTime:非核心线程存活保持时间为0s,workQueue:该线程池中的任务队列为LinkedBlockingQueue
- newSingleThreadExecutor:创建一个单线程的线程池,该线程池中只有一个线程,适用于需要保证按照指定顺序执行各个任务。
corePoolSize:核心线程数为1,maximumPoolSize:线程池最大大小为1,keepAliveTime:非核心线程存活保持时间为0s,workQueue:该线程池中的任务队列为LinkedBlockingQueue
- newScheduledThreadPool:适用于执行延时或者周期性任务。
corePoolSize:核心线程数为corePoolSize(自定义),maximumPoolSize:线程池最大大小为Integer.MAX_VALUE(2147483647),keepAliveTime:非核心线程存活保持时间为0s,workQueue:该线程池中的任务队列为DelayedWorkQueue
- newCachedThreadPool:可缓存线程池,用来创建一个可以无限扩大的线程池,适用于负载较轻的场景,执行短期异步任务。
-
一个简单的例子
class NumberThread implements Runnable{
@Override
public void run() {
for (int i = 0;i<=100;i++){
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
}
class NumberThread1 implements Runnable{
@Override
public void run() {
for (int i = 0;i<=100;i++){
if(i % 2 != 0){
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
}
public class ThreadPool {
public static void main(String[] args) {
//1 提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
//service.execute();执行任务,没有返回值,适合用于Runnable
//service.submit();执行任务,有返回值,适合用于Callable
//2 执行指定的线程操作,需要提供实现Runable接口或Callable接口实现类的对象
service.execute(new NumberThread());
service.execute(new NumberThread1());
//3 关闭连接池
service.shutdown();
}
}
线程安全问题
-
问题出现的原因:当某个线程操作共享数据的过程中,操作尚未完成时,其他线程参与进来,也操作共享数据
-
如何解决:当一个线程在操作共享数据的时候,其他线程不能参与进来,直到线程a操作完之后,其他线程才可以开始操作,这种情况即使线程a出现了阻塞也不能改变。在java中通过同步机制,来解决线程的安全问题。
-
同步代码块
synchronized(同步监视器){
需要被同步的代码
}
1. 操作共享数据的代码即为需要被同步的代码
2. 共享数据:多个线程共同操作的变量。
3. 同步监视器: 也称为锁,任何一个类的对象都可以充当锁,要求:多个线程必须要共用一把锁
-
同步方法
如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的 -
Lock锁
ReentrantLock有两个构造器,一个空参,一个Boolean fair公平锁,默认的是非公平锁,将fair设为True是为公平锁
import java.util.concurrent.locks.ReentrantLock;
class Windows implements Runnable{
private int ticket = 1000;
//1 实例化ReentrantLock
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while(true){
try{
//2 调用锁定方法lock()
lock.lock();
if(ticket > 0){
//try {
// Thread.sleep(100);
//} catch (InterruptedException e) {
// e.printStackTrace();
//}
System.out.println(Thread.currentThread().getName()+"售出:"+ticket);
ticket--;
}else{
break;
}
}finally {
//3 调用解锁方法unlock()
lock.unlock();
}
}
}
}
public class LockTest {
public static void main(String[] args) {
Windows w1 = new Windows();
Thread t1 = new Thread(w1);
Thread t2 = new Thread(w1);
Thread t3 = new Thread(w1);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
- synchronized 与 Lock 的异同?
同: 二者都可以解决线程安全问题
不同: synchronized机制在执行完相应的同步代码块后自动释放同步监视器
Lock需要手动的启动同步(lock()),同时结束同步也需要手动实现(unlock())
关于java中锁的知识还没有足够的了解,目前先写到这里,如果文中有错误的地方欢迎大家指出交流,互相学习。