多线程介绍

一、多线程入门
1、线程与进程区别:
进程是所有线程的集合,每一个线程是进程中的一条执行路径;

2、多线程优点:
多线程的好处提高程序的效率;

3、多线程创建方式:

继承Thread类 重写run方法
public class Main {
public static void main(String[] args) {
new MyThread().start();
}
}
class MyThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + “\t” + Thread.currentThread().getId());
}
}

实现java.lang.Runnable接口,重写run()方法,然后使用Thread类来包装
public class Main {
public static void main(String[] args) {
// 将Runnable实现类作为Thread的构造参数传递到Thread类中,然后启动Thread类
MyRunnable runnable = new MyRunnable();
new Thread(runnable).start();
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + “\t” + Thread.currentThread().getId());
}
}

实现Callable接口,重写call()方法,然后包装成java.util.concurrent.FutureTask, 再然后包装成Thread
public class Main {
public static void main(String[] args) throws Exception {
// 将Callable包装成FutureTask,FutureTask也是一种Runnable
MyCallable callable = new MyCallable();
FutureTask futureTask = new FutureTask<>(callable);
new Thread(futureTask).start();
// get方法会阻塞调用的线程
Integer sum = futureTask.get();
System.out.println(Thread.currentThread().getName() + Thread.currentThread().getId() + “=” + sum);
}
}
class MyCallable implements Callable {
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName() + “\t” + Thread.currentThread().getId() + “\t” + new Date() + " \tstarting…");
int sum = 0;
for (int i = 0; i <= 100000; i++) {
sum += i;
}
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName() + “\t” + Thread.currentThread().getId() + “\t” + new Date() + " \tover…");
return sum;
}
}

三种方式比较:

  • Thread: 继承方式, 不建议使用, 因为Java是单继承的,继承了Thread就没办法继承其它类了,不够灵活
  • Runnable: 实现接口,比Thread类更加灵活,没有单继承的限制
  • Callable: Thread和Runnable都是重写的run()方法并且没有返回值,Callable是重写的call()方法并且有返回值并可以借助FutureTask类来判断线程是否已经执行完毕或者取消线程执行
  • 当线程不需要返回值时使用Runnable,需要返回值时就使用Callable,一般情况下不直接把线程体代码放到Thread类中,一般通过Thread类来启动线程
  • Thread类是实现Runnable,Callable封装成FutureTask,FutureTask实现RunnableFuture,RunnableFuture继承Runnable,所以Callable也算是一种Runnable,所以三种实现方式本质上都是Runnable实现

4、线程状态

  • 创建(new)状态: 准备好了一个多线程的对象,即执行了new Thread(); 创建完成后就需要为线程分配内存
  • 就绪(runnable)状态: 调用了start()方法, 等待CPU进行调度
  • 运行(running)状态: 执行run()方法
  • 阻塞(blocked)状态: 暂时停止执行线程,将线程挂起(sleep()、wait()、join()、没有获取到锁都会使线程阻塞), 可能将资源交给其它线程使用
  • 死亡(terminated)状态: 线程销毁(正常执行完毕、发生异常或者被打断interrupt()都会导致线程终止)

二、多线程同步
线程三大特性:原子性、可见性、有序性

1、同步方法(同步函数)
synchronized关键字修饰方法。由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。

代码如下:
public synchronized void save(){}

注:synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类(静态同步函数)
synchronized 修饰方法使用锁是当前this锁
synchronized 修饰静态方法使用锁是当前的字节码文件

2、同步代码块
synchronized关键字修饰的语句块。被该关键字修饰的语句块会自动加上内置锁,从而实现同步

代码如下:
synchronized(object){}

注:同步是一种高开销的操作,因此应该尽量减少同步内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可

3、使用特殊域变量(volatile)实现线程同步

  • a.volatile关键字为域变量的访问提供一种免锁机制
  • b.使用volatile修饰域相当于告诉虚拟机该域可能被其他现象更新
  • c.因此每次使用该域就要重新计算,而不是使用寄存器中的值
  • d.volatile不会提供任何原子操作,它也不能用来修饰final类型的变量

注:多线程中的非同步问题主要出现在对域的读写上,如果让域自身避免这个问题,则就不需要修改操作该域的方法。用final域,有锁保护的域和volatile域可以避免非同步的问题

4、使用局部变量实现线程同步
如果使用ThreadLocal管理变量,则每一个使用变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。

ThreadLocal类的常用方法:

  • ThreadLocal():创建一个线程本地变量
  • get():返回此线程局部变量的当前线程副本中的值
  • initialValue():返回此线程局部变量的当前线程的“初始值”
  • set(T value):将此线程局部变量的当前线程副本中的值设置为value

//只改Bank类,其余代码与上同
public class Bank{
//使用ThreadLocal类管理共享变量account
private static ThreadLocal account = new ThreadLocal(){
@Override
protected Integer initialValue(){
return 100;
}
};
public void save(int money){
account.set(account.get()+money);
}
public int getAccount(){
return account.get();
}
}

注:ThreadLocal与同步机制
a.ThreadLocal与同步机制都是为了解决多线程中相同变量的访问冲突问题
b.前者采用以“空间换时间”的方法,后者采用以“时间换空间”的方式
c、ThreadLocal通过map集合实现

5、使用原子变量实现线程同步
在java的util.concurrent.atomic包中提供了创建了原子类型变量的工具类,使用该类可以简化线程同步。

其中AtomicInteger 表可以用原子方式更新int的值,可用在应用程序中(如以原子方式增加的计数器),但不能用于替换Integer;可扩展Number,允许那些处理机遇数字类的工具和实用工具进行统一访问。

AtomicInteger类常用方法:

  • AtomicInteger(int initialValue) : 创建具有给定初始值的新的AtomicInteger
  • addAddGet(int dalta) : 以原子方式将给定值与当前值相加
  • get() : 获取当前值

6、使用重入锁实现线程同步
在javaSE5.0新增了一个java.concurrent包来支持同步。ReentrantLock类可以重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和快具体相同的基本行为和语义,并且扩展了其能力

ReentrantLock类的常用方法有:

ReentrantLock():创建一个ReentrantLock实例 lock():获得锁 unlock():释放锁

注:ReentrantLock()还有一个可以创建公共锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用

  class Demo{
  		private int num= 100;
     	private Lock lock = new ReentrantLock();
          public int getAccount() {
         		return num;
      		 }
       //这里不需要synchronized 
      public void save(int count) {
         lock.lock();
         try{
             num+= count;
          }finally{
             lock.unlock();
        }             
   }

注:关于Lock对象和synchronized关键字的选择:
a.最好两个都不用,使用一种java.util.concurrent包提供的机制,能够帮助用户处理所有与锁相关的代码
b.如果synchronized关键字能满足用户的需求,就用synchronized,因为它能简化代码
c.如果需要更高级的功能,就用ReentrantLock类,此时要注意及时释放锁,否则会出现死锁,通常在finally代码释放锁

三、多线程通讯
1、wait(), notify()和notifyAll()
在Object.java中,定义了wait(), notify()和notifyAll()等接口。

  • wait()的作用是让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁。
  • notify()和notifyAll()的作用,则是唤醒当前对象上的等待线程;notify()是唤醒单个线程,而notifyAll()是唤醒所有的线程。

2、wait与sleep区别
对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的。

sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。

在调用sleep()方法的过程中,线程不会释放对象锁。

而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备
获取对象锁进入运行状态。

3、停止线程

  • 方式一:使用退出标识,使得线程正常退出,即当run方法完成后进程终止。

public void run() {
while(flag){
//do something
}
}

  • 方式二
    使用stop强行中断线程(此方法为作废过期方法),不推荐使用,暴力终止,可能使一些清理性的工作得不到完成。还可能对锁定的内容进行解锁,容易造成数据不同步的问题。

  • 方式三
    使用interrupt方法中断线程。

4、守护线程
Java中有两种线程,一种是用户线程,另一种是守护线程。
当进程不存在或主线程停止,守护线程也会被停止。
使用setDaemon(true)方法设置为守护线程

5、join()、priority、yield()

  • join作用是让其他线程变为等待
  • 线程中,通过一个int priority来控制优先级,范围为1-10,其中10最高,默认值为5
  • Thread.yield()方法的作用:暂停当前正在执行的线程,并执行其他线程。(可能没有效果)

6、AtomicInteger
AtomicInteger是一个提供原子操作的Integer类,通过线程安全的方式操作加减。

AtomicInteger是在使用非阻塞算法实现并发控制,在一些高并发程序中非常适合,但并不能每一种场景都适合,不同场景要使用使用不同的数值类。(不能保证各线程使用的顺序)

public class Sample1 {
private static Integer count = 0;
synchronized public static void increment() {
count++;
}
}

7、ThreadLoca

  • ThreadLocal提高一个线程的局部变量,访问某个线程拥有自己局部变量。
  • 当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

ThreadLocal类接口很简单,只有4个方法,我们先来了解一下:

  • void set(Object value)设置当前线程的线程局部变量的值。
  • public Object get()该方法返回当前线程所对应的线程局部变量。
  • public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
  • protected Object initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。
      
      值得一提的是,在JDK5.0中,ThreadLocal已经支持泛型,该类的类名已经变为ThreadLocal。API方法也相应进行了调整,新版本的API方法分别是void set(T value)、T get()以及T initialValue()。
     
    ThreadLoca通过map集合实现。

8、java内存模型
JMM是一种规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。目的是保证并发编程场景中的原子性、可见性和有序性。

9、线程池
线程池是指在初始化一个多线程应用程序过程中创建一个线程集合,然后在需要执行新的任务时重用这些线程而不是新建一个线程。线程池中线程的数量通常完全取决于可用内存数量和应用程序的需求。然而,增加可用线程数量是可能的。线程池中的每个线程都有被分配一个任务,一旦任务已经完成了,线程回到池子中并等待下一次分配任务。

线程池作用:

  • (1)、降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
  • (2)、提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行;
  • (3)方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换(cpu切换线程是有时间成本的(需要保持当前执行线程的现场,并恢复要执行线程的现场))。
  • (4)提供更强大的功能,延时定时线程池

线程池四种创建方式:
Java通过Executors(jdk1.5并发包)提供四种线程池,分别为:

  • newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
  • newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
  • newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
  • newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收。
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
cachedThreadPool.execute(new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName() );
}
});

创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
ExecutorService newCachedThreadPool = Executors.newFixedThreadPool(3);
newCachedThreadPool.execute(new Runnable() {
public void run() {
try {
Thread.sleep(1000);
} catch (Exception e) {
// TODO: handle exception
}
System.out.println(Thread.currentThread().getName() );
}
});

创建一个定长线程池,支持定时及周期性任务执行。
ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(5);
newScheduledThreadPool.schedule(new Runnable() {
public void run() {
System.out.println(“delay 3 seconds”);
}
}, 3, TimeUnit.SECONDS);

创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
newSingleThreadExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println(“index:” + index);
try {
Thread.sleep(200);
} catch (Exception e) {
// TODO: handle exception
}
}
});

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值