多线程基本操作
一、线程是什么
程序用我们程序员的话来说就是一段静态的代码,静态对象。而正在运行的程序,或者说程序的一次执行过程,这种动态的过程就叫做进程。接下来让我们把进程进一步的划分,就是线程了。
线程可以说是进程的进一步细化,指的是程序内部的一条执行路径,一个进程中的多个线程共享相同的内存单元/内存地址空间—>简单来说就是他们从同一堆中分配对象,可以访问相同的变量和对象。
java程序中最少有两个线程:
1.JVM GC垃圾回收机制(守护线程/后台线程)
2.main线程/main线程组
二、线程的操作
1.创建线程
1.创建一个线程对象十分简单,只需要new Thread()就可以创建好一个线程对象。
2.然后调用start()方法即可开启线程。调用start()方法后线程会自动执行run()方
法。
3.注意:调用start()方法启动线程而不是直接调用run方法。
举例简单创建一个线程对象并调用:
public class ConcurrentTest {
public static void main(String[] args) {
//这里采用匿名内部类的写法
Thread thread = new Thread(){
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"执行了");
}
};
thread.setName("线程1");
thread.start(); //切记使用start()方法开启线程而不是直接选择调用run()方法。
}
}
>继承Thread
public class MyThread extends Thread {
//重写方法
@Override
public void run() {
System.out.println("可以执行");
}
public static void main(String[] args) {
new MyThread().start(); //启动线程
}
}
>实现Runnable接口
public class MyThread implements Runnable {
@Override
public void run() {
//Thread.currentThread():当前线程
//getName():获取当前线程的名字
System.out.println(Thread.currentThread().getName()+"正常执行");
}
public static void main(String[] args) {
//此时传递的name是线程的名字,也可以不传递name
new Thread(new MyThread(),"线程2").start();
}
}
三、线程状态
线程的状态都在Thread中的state枚举中定义
每种线程状态的含义:
New(新建):当一个线程对象被创建但还未调用 start() 方法来启动线程时,线程处于新建状态。此时线程已经分配了内存空间并初始化完毕,但还没有启动它的执行代码。
Runnable(可运行):线程状态为可运行,表示线程正在 JVM 中执行或等待 CPU 执行。当调用了 start() 方法后,线程就进入了可运行状态。在可运行状态下,线程可能正在等待其他系统资源,如 I/O、网络连接等,并不一定是一直占用 CPU 资源。
Blocked(阻塞):当线程处于阻塞状态时,表示该线程暂时无法获取所需的锁,因此无法继续执行。常见的原因包括:等待锁、被其他线程调用了 wait() 方法、等待输入/输出(I/O)操作完成等。
Waiting(等待):当线程处于等待状态时,表示该线程需要等待其他线程通知它去唤醒自己才能继续执行。常见的情况包括:等待其他线程的操作、调用了 wait() 方法而进入等待状态、调用了 join() 方法等待另一个线程执行完毕等。
Timed Waiting(计时等待):和等待状态类似,但是计时等待状态有超时时间,当等待的时间超过了指定的时间后就会自动恢复到 Runnable 状态。示例包括:调用了 sleep() 方法、调用了 join() 方法并指定了超时时间、调用了 wait() 方法并指定了超时时间等。
Terminated(终止):线程结束时会进入终止状态,其原因可能是执行完了该线程的任务或者出现了异常而导致线程意外终止。
JConsole使用(课外)
JConsole 是 idea 安装的时候就有的,如果找不到可以直接打开Win10的搜索
然后输入: JConsole,然后点击运行
选择你的进程连接,点击不安全连接
找到我们的 main 线程,可以看到 WAITING
要使用 wait 方法,就必须搭配使用 synchronized 方法,否则就会抛出异常
public class wait {
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
synchronized (object){
System.out.println("wait之前");
object.wait();//线程堵塞停留在wait
System.out.println("wait之后");
}
}
}
这个异常就是,"非法监视器" 异常 ,为什么会出现这个异常呢?
因为: wait 方法的操作分为三步
第一步:先释放锁
第二步:进行阻塞等待
第三步:收到通知之后,重新尝试获取锁,并且在获取锁之后,继续往下执行
而 synchronized 方法,就是进行加锁,如果没有 synchronized 方法,即是没有进行加锁,也就是无法满足 wait 方法的第一步:获取锁,自然也就无法继续进行后续的步骤.
synchronized 方法的括号里面放的是你要进行加锁的线程,比如上面的代码, 里面放的是 object
Object的wait()方法并不是可以随便调用的。它必须包含在对应的synchronized语句中,无论是wait()或notify()都需要首先获得目标对象的一个监视器。
四、等待线程结束(join)和线程谦让(yield)
等待线程结束业务逻辑:
一个线程的输入依赖另一个或多个线程的输出,表明该线程需要依赖的线程执行结束之后才能继续执行。
jdk中提供的方法join就是解决这种逻辑的。
void join() //等待该线程终止
void join(long time) //等待该线程终止,等待时间最长为time毫秒
简单说:ThreadA需要在线程B结束后执行,就可以在线程A中调用ThreadB.join()方法。
举例:
public class MyThreadC implements Runnable{
static long start = 0;
@Override
public void run() {
try {
start = System.currentTimeMillis();
Thread.sleep(2000);
}catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Thread thread = new Thread(new MyThreadC());
thread.start();
try {
thread.join();
}catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("相差时间为"+(System.currentTimeMillis()-start)/1000);
}
}
// 控制台打印:相差时间为2
//
// 如果将thread.join() ==>替换为 thread.join(1000);//表明等待该线程终止的最长时间为1秒。1秒后主线程获取资源直接执行不在等待线程C的结束。
// 控制台打印:相差时间为1
线程的谦让yield
static void yield() //暂停当前线程,执行其它线程
线程yield()方法会让当前线程让出cpu,但是需要注意的是并不代表接下来该线程
不抢占cpu资源,它还是会和其他线程竞争cpu资源的。该方法经常使用在线程优先级低不重要的线程过多的占用cpu资源。可以调用该方法yield();
五、volatile
保证内存可见性
说到内存可见性就必须要提到Java的内存模型,如下图所示:
如上图所示,所有线程的共享变量都存储在主内存中,每一个线程都有一个独有的工作内存,每个线程不直接操作在主内存中的变量,而是将主内存上变量的副本放进自己的工作内存中,只操作工作内存中的数据。当修改完毕后,再把修改后的结果放回到主内存中。每个线程都只操作自己工作内存中的变量,无法直接访问对方工作内存中的变量,线程间变量值的传递需要通过主内存来完成。
上述的Java内存模型在单线程的环境下不会出现问题,但在多线程的环境下可能会出现脏数据。
volatile可见性是通过汇编加上Lock前缀指令,触发底层的MESI缓存一致性协议来实现的。当然这个协议有很多种,不过最常用的就是MESI。(MESI协议在上篇写出)
禁止指令重排序
指令的执行顺序并不一定会像我们编写的顺序那样执行,为了保证执行上的效率,JVM(包括CPU)可能会对指令进行重排序
不保证原子性
volatile并不能代替锁,它也无法保证一些复合操作的原子性。比如通过volatile是无法保证i++的原子性操作的。对volatile修饰的变量进行的操作,不保证多线程安全。
六、synchronized
Synchronized是Java中的一个关键字,被称为“同步锁”。顾名思义,它是一种锁,当某一时刻有多个线程对同一段程序进行操作时,能够保证只有一个线程能够获取到资源,因此保证了线程安全。
Synchronized主要有三种使用方式:
- 修饰普通方法,锁作用于当前对象实例。
- 修饰静态方法,锁作用于类的Class实例。
- 修饰代码块,作用于当前对象实例,需要指定加锁对象。
普通方法:
Synchronized是一个关键字,当作用于一个普通方法的时候,这个方法便被加上了同步锁,意味着某一时刻只有一个线程可以操作访问这个方法
public class fancySyncTest {
public synchronized void method1(){
try {
System.out.println(Thread.currentThread().getName());
Thread.sleep(3000);
}catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException{
final fancySyncTest fs = new fancySyncTest();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
fs.method1();
}
},"线程1获取到资源");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
fs.method1();
}
},"线程2获取资源");
t1.start();
t2.start();
}
}
这段代码有方法method1( ),它的功能就是打印出当前执行这段代码的线程,并且让它休眠5秒钟。然后我们开启两个线程,线程t1和t2,它们两个一起启动
这段代码的执行顺序很简单,线程1或者线程2任意一个线程先去执行method1( )的内容,然后休眠5秒钟。完事就释放锁,下一个线程会继续执行
静态方法:
静态方法和普通方法的区别只有一个,就是Synchronized关键字是作用于静态方法的。但是仅仅这个区别,代表着锁的对象也是不同的。原因在于Java的静态关键字它和实例对象没有任何关系,它作用的资源是在类的初始化时期就加载的,所以它只能由每个类唯一的Class对象调用。当它作用于一个Class对象时,它就会将这一整个类都锁住,简称"类锁"
public class fancySyncTest {
public static synchronized void method1(){
try {
System.out.println(Thread.currentThread().getName());
Thread.sleep(3000);
}catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException{
final fancySyncTest fs = new fancySyncTest();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
fs.method1();
}
},"线程1获取到资源");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
fs.method1();
}
},"线程2获取资源");
t1.start();
t2.start();
}
}
这段由静态关键字修饰的代码和普通的方法没什么区别,唯一的区别就在于:由于method1( )是静态的,所有我们不用创建对象,可以直接由类实例fancySynTest直接调用!作为代价,这把锁锁住的对象也是直接覆盖了这个类。也就是说,当线程1执行的时候,没有别的线程可以访问这个fancySyncTest类。
Synchronized修饰普通方法和静态方法的区别只有一个:粒度不同。类似于数据库中表锁和行锁的区别。
代码块:
public class fancySyncTest {
public synchronized void method1(){
synchronized (this) {
// 逻辑代码
}
}
}
代码块锁住的对象就是后面括号里的东西。比如这里的synchronized (this),意味着只有当前对象才可以访问这段代码块。
synchronized锁的优化策略:
加锁流程顺序:偏向锁加锁——>轻量级锁——>自旋锁——>重量级锁
从jdk1.6开始Java团队就对它进行了优化。通过各种各样的手段,如自旋锁、偏向锁、轻量级锁等技术来减少锁的开销
1.偏向锁
Java偏向锁是Java6引入的一项多线程优化。
偏向锁是JVM认为没有发生并发的场景下提供的锁。它是JDK 1.6中的重要引进,因为HotSpot团队发现在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低,引进了偏向锁。
2.轻量级锁
轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁
当一个线程在获取锁的时候,如果该锁已被其它线程获取到,那么该线程就会去循环自旋获取锁,不停地判断该锁是否能够已经被释放,自选直到获取到锁才会退出循环。通常该自选在源码中都是通过for(; ;)或者while(true)这样的操作实现,非常粗暴。
自旋就代表着占用cpu资源,使用自旋锁的目的是为了避免线程在阻塞和唤醒状态之间切换而占用资源,毕竟线程从阻塞状态切换到唤醒状态需要CPU从用户态转化为内核态,而频繁的状态切换就会导致CPU的资源浪费,所以引入了自选锁。
3.锁消除
锁消除指的是虚拟机即时编译器在运行的时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行削除。那么如何确定这个锁是否需要进行削除?主要来源于逃逸分析的判断,如果判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把它们当作栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。
锁消除涉及的一项关键技术为逃逸分析。所谓逃逸分析就是观察某一个变量是否会逃出某一个作用域。
Synchronized重量锁涉及的核心组件:
Waiting Queue
调用wait方法被阻塞的线程被放置在这里;
Contention(竞争)
竞争队列,所有请求锁的线程首先被放在这个竞争队列中;
ListEntry(进入)
Contention List中那些有资格成为候选资源的线程被移动到Entry List中;
ListOnDeck(在甲板上)
任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为OnDeck;
Owner
当前已经获取到锁资源的线程被称为Owner;
!Owner
当前释放锁的线程。
七、无锁
CAS
在Unsafe类里,包含着CAS的操作函数。它采用无锁的乐观策略,由于其非阻塞性,它对死锁问题天生免疫,并且,线程间的相互影响也远远比基于锁的方式要小得多。更为重要的是,使用无锁的方式完全没有锁竞争带来的系统开销,也没有线程间频繁调度带来的开销。因此,它要比基于锁的方式拥有更优越的性能。
CAS算法的过程:
●它包含四个参数CAS(object, offset, expectdValue, newValue),分别是:
object:待更新的对象
offset:待更新变量的
offset偏移量
expectdValue:表示预期值
newValue:表示新值
●那么,仅当object和offset定位到的值等于expectdValue值时,才会将其值设为newValue,如果与expectdValue值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值。
●在硬件层面,大部分的现代处理器都已经支持原子化的CAS指令。