一 常用概念与特性
1 并发与并行的概念
并行:多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时。
并发:通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。
2 线程安全与不安全的概念
线程安全:经常用来描绘一段代码。指在并发的情况之下,该代码经过多线程使用,线程的调度顺序不影响任何结果。这个时候使用多线程,我们只需要关注系统的内存,cpu是不是够用即可。反过来,线程不安全就意味着线程的调度顺序会影响最终结果,如不加事务的转账代码。
3 同步的概念
同步:Java中的同步指的是通过人为的控制和调度,保证共享资源的多线程访问成为线程安全,来保证结果的准确。如一些代码简单的加入@synchronized关键字保证结果的准确性。在保证结果准确的同时,提高性能,才是优秀的程序。线程安全的优先级高于性能。
4 线程的各种状态
线程的六大状态(一个线程在一个时刻只能处于一个状态)
1) NEW(new新建状态):
当一个线程创建好后,没有调用start方法时所处于的状态。
2) RUNNABLE(runnable就绪状态):
当线程调用了start方法后,该线程就会被放在可运行的线程池中,去争取CPU的时间片。
3) BLOCKED(blocked):
处于阻塞状态的线程在等待monitor锁。
4) WAITING(waiting):
一个线程在等待另一个线程执行一个动作时在这个状态。
当该线程调用wait()方法或者其他线程在该线程里面调用join()时,线程所处于的状态(注意,这两个方法都没有参数)。处于该状态的线程如果没有被唤醒,将处于无限等待的状态。
wait():
5) TIMED_WAITING(timed_waiting):
一个线程在一个特定的等待时间内等待另一个线程完成一个动作会在这个状态。
当该线程调用sleep方法或者调用wait(long)方法,或者其他线程在里面调用join(long)方法时,线程所处于的状态。该状态虽然会等待,但设置了时间限制,不会无限等待,当超过时间以后会自动唤醒。
6) TERMINATED(terminated):
线程运行完后的状态。
5 线程状态之间的转换
当一个线程创建以后,就处于新建状态。那什么时候这个状态会改变呢?只要它调用的start()方法,线程就进入了锁池状态。
进入锁池以后就会参与锁的竞争,当它获得锁以后还不能马上运行,因为一个单核CPU在某一时刻,只能执行一个线程,所以他需要操作系统分配给它时间片,才能执行。所以人们通常把一个线程在获得锁后,获得系统时间片之前的状态称之为可运行状态(就绪状态),但Java源代码里面并没有可运行状态这一说。
当一个持有对象锁的线程获得CPU时间片以后,开始执行这个线程,此时叫做运行状态。
当一个线程正常执行完,那么就进入终止(死亡)状态。系统就会回收这个线程占用的资源。
但是,线程的执行并不是那么顺利的。一个正在运行的线程有可能会进入I/O交互,还可能调用sleep()方法,还有可能在当前线程当中有其它线程调用了join()方法。这时候线程就进入了阻塞状态(但这也只是我们在理解的时候自己加上去的,源代码里也没有定义这一个状态)。阻塞状态的线程是没有释放对象锁的。当I/O交互完成,或sleep()方法完成,或其它调用join()方法的线程执行完毕,阻塞状态的线程就会恢复到可运行状态,此时如果再次获得CPU时间片就会进入运行状态。
一个处于运行状态的线程还可能调用wait()方法、或者带时间参数的wait(long milli)方法。这时候线程就会将对象锁释放,进入等待队列里面(如果是调用wait()方法则进入等待状态,如果是调用带时间参数的则进入定时等待状态)。
一个线程如果的调用了带时间参数的wait(long milli)方法进入了定时等待状态,那么只要时间一到就会进入锁池状态,并不需要notify()或notifyAll()方法来唤醒它。如果调用的是不带时间参数的wait()则需要notify()或notifyAll()这两个方法来唤醒它然后进入锁池状态。进入锁池状态以后继续参与锁的竞争。
当一个处于运行状态的线程调用了suspend()方法以后,它就会进入挂起状态(这一方法已经过时不建议使用)。挂起状态的线程也没有释放对象锁,它需要调用resume()方法以后才能恢复到可运行状态。将线程挂起容易导致程序死锁。
6 线程的优先级
每个java线程都有一个优先级,优先级为1--10,默认为5。优先级越高只能保证这个线程执行的概率越大,但不能百分之百保证他就一定执行。
7 线程死锁
线程死锁
死锁:如果一个线程集合里面的每个线程都在等待这个集合中的其他一个线程(包括自身)才能继续往下执行,若无外力他们将无法推进,这种情况就是死锁,处于死锁状态的线程称为死锁进程
死锁的四个必要条件:
1)互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
2)请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
3)不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
4)环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。
二 线程使用和常用类
1 线程的创建方式
方式1:
package com.zyb.test;
public class MyThread extends Thread{
@Override
public void run() {
System.out.println("调用自定义线程run方法");
super.run();
}
}
package com.zyb.test;
public class TestThread {
public static void main(String[] args) {
//创建线程自定义的对象
MyThread myThread = new MyThread();
//开启线程,由虚拟机执行该线程对象的run方法。
myThread.start();
}
}
方式2:
package com.zyb.test;
public class MyThread1 implements Runnable{
@Override
public void run() {
System.out.println("调用该线程的run方法");
}
}
package com.zyb.test;
public class TestThread {
public static void main(String[] args) {
//使用runnable的实现类创建一个线程对象
Thread thread = new Thread(new MyThread1());
//开启线程,由虚拟机执行该线程对象的run方法。
thread.start();
}
}
2 Thread
1)构造方法
public Thread(Runnable target):使用Runnable的子类对象创建一个线程对象。
public Thread(Runnable target, String name):使用runnable封装一个线程,并给该线程命名为name;
public Thread(ThreadGroup group, Runnable target, String name):使用runnable封装一个线程,并给该线程命名为name,并将创建的线程放入线程组group中。
2)静态方法
public static native Thread currentThread():获取当前线程对象。
public static native void yield():当前线程转让出cpu的控制权(切换)。该方法与sleep有点相似,也不会释放锁,只是将当前线程放弃时间片,重新变就绪状态。
public static native void sleep(long millis) throws InterruptedException:将线程暂停一段时间。一定是有当前线程调用,当线程调用后,线程会放弃CPU执行权,但不会释放锁,当在指定时间过后,又变回就绪状态,去争取时间片去执行。
3)实例方法
public final void setPriority(int newPriority):设置当前线程的优先级。优先级越高越有机会调用。
public final int getPriority():获取线程的优先级。
public final synchronized void setName(String name):设置线程的名字。
public final String getName():获取线程的名字。
public long getId():获取线程的id
public final void setDaemon(boolean on):设置该线程是否是守护线程。
public void interrupt():中断线程。
public final void join() throws InterruptedException:在一个线程内让另一个线程对象调用该方法,必须让那个调用join方法的线程完了过后再执行该线程。
注意:该方法在那个线程里面调用,就会阻塞该线程,必须在调用该方法的线程结束过后才会继续调用阻塞线程。
例如:在线程A中先开启线程B,C,开启过后B调用join方法,这时线程A进入阻塞状态,然后线程B,和线程C,并发执行,知道B执行完后,线程A才继续执行。
public final boolean isDaemon():判断是否是守护线程。
public final ThreadGroup getThreadGroup():获取该线程的线程数组对象。
什么是守护线程:
线程分为用户线程和守护线程,创建的线程一般默认就是用户线程,如果该线程调用了setDaemon(true)方法时,就会变为守护线程。
守护线程就是为了维护其他线程而存在的,当jvm中只有守护线程而没有用户线程时,守护线程就失去了作用,守护线程就会结束。如果创建的线程对象没有显示的调用setDaemon方法,默认就是一个用户线程。
守护线程实例:
垃圾回收线程就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。
守护线程的生命周期:
守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。也就是说守护线程不依赖于终端,但是依赖于系统,与系统“同生共死”。那Java的守护线程是什么样子的呢。当JVM中所有的线程都是守护线程的时候,JVM就可以退出了;如果还有一个或以上的非守护线程则JVM不会退出。
3 ThreadLocal
关于ThreadLocal的概念很多,这里只说本人在使用中对其的理解;首先我们需要知道,Thread有个threadLocals 属性,如下
ThreadLocal.ThreadLocalMap threadLocals = null;
该属性是每个线程都有的属性(虽然该属性不是没有实现Map接口,但是可以将其当成一个Map来理解),可以往其中放值,放入其中的值,只要线程没有结束,该线程就能够取出来使用,其他线程是无法调用的。个人理解ThreadLocal就是一个操作线程ThreadLocalMap属性的工具和钥匙,说它是工具是因为它具有操作当前线程ThreadLocalMap的能力,能往其中存取值。说它是钥匙是因为在往ThreadLocalMap中添加值时,将ThreadLocal本身作为key传入进去,意思也就是说,使用那个ThreadLocal放入的值,也只能通过该ThreadLocal获取,其他的ThreadLocal是不能获取的。由于是用ThreadLocal作为的key,所以一个ThreadLocal只能往线程中存取一个值。如果没有理解,请看下面方法源码。
ThreadLocal的主要方法:
1)public T get() :
源码如下:
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
//以该ThreadLocal作为key获取值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//没有map与值的话返回null
return setInitialValue();
}
2) public void set(T value):
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
//如果有map,将当前ThreadLocal作为key将值放入进去
map.set(this, value);
else
//如果map不存在,创建map并将当前ThreadLocal作为key将值放入进去
createMap(t, value);
}
3)protected T initialValue()
protected T initialValue() {
return null;
}
当调用get方法时,如果没有值,默认返回null,如果不想返回空,需要自己手动重写该方法。
4) public void remove():
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
//将该ThreadLocal存入的值从当前线程的ThreadLocalMap里面移除。
m.remove(this);
}
由于ThreadLocalMap是与线程绑定的,所以里面存储的值是与线程的生命周期保持一致的,比如如果使用了线程池,将线程进行复用的话,不使用remove对其进行删除的话,存储的值的生命周期将和线程同步。
4 Synchronized
使用原理
synchronized是一种同步锁,它的修饰对象有以下几种。
1)、修饰一个代码块:当修饰一个代码块时,他会将该对象的该段代码块进行上锁,当一个线程在执行一个对象的该代码块,那么另一个线程就不能执行这个对象的该代码块(注意是同一个对象的代码块),必须等前一个线程执行完后,后一个线程才可以去执行。
注意:当修饰代码块时,可以指定修饰那个对象的代码块。
注意:synchronized (Object u) {}当u是对象时,表示只有在获取了对象u的锁以后才能执行{}里面的方法。当u是一个类对象时,表示只有在获取了该类的锁以后才能执行{}里面的方法。关于对象锁与类锁,请看3
2)、修饰一个方法:当修饰一个方法时,他会将该对象的该方法进行上锁,如果两个线程都要调用该对象的该方法就不会同时访问,只能等一个线程访问过后,另一个线程才能执行。
注意:一个对象只有一个锁,当一个线程访问该对象的上锁的代码A或者方法A时,他会先获取该对象的锁,只有获取到过后才会执行上锁代码,如果另一个线程要访问该对象的上锁代码B或者方法B时,如果该对象的锁被其他线程获取,那么就不能执行上锁代码B或者方法B。总之,如果一个对象有两个方法被上锁,但对象只有一个锁,所以这两个方法不能同时在两个线程中调用,只有等一个线程调用完以后释放对象锁,另一个线程获取对象锁后才能执行另一个方法。
注意:synchronized可以被继承,只是如果你要覆盖该方法的话,必须将该方法显示的写出synchronized,才有上锁的作用。
**3)
、修饰一个静态的方法:**当修饰一个静态方法时,他会将该类的该静态方法进行上锁,如果两个线程都要调用该类的该静态方法就不会同时访问,只能等一个线程访问过后,另一个线程才能执行。
注意:如果是静态方法上加锁,它作用于所有的对象,当在一个线程中通过对象调用该静态方法或通过类名调用,那么另一线程就不能调用该类的另一个静态方法(不管是通过类名调用还是通过对象调用),但对上锁的非静态方法没有影响,在另一个线程中任然能执行上锁的非静态方法,不管其他线程是否使用上锁的静态方法。
我的理解就是每一个对象有一个锁,每一个类也有一个锁,当一个线程要通过对象访问类的上锁非静态方法时,就要获取该对象的锁才能访问,如果锁被其他线程获取,这个线程只能等待其他线程释放对象锁。当一个线程通过对象或者类访问类的上锁静态方法时,就要获取该类的锁才能访问。
注意对象锁和类锁是两个不同的锁,就是当类锁被其他线程获取过后,当前线程任然可以获取对象锁调用上锁的非静态方法。
以上都是对上锁的方法进行的讨论,如果方法没有上锁,那么任意的线程都可以访问该方法,不用获取对象或者类的锁。
心得:当要进入synchronized的代码块时,必须先获取()内对象或者类的锁,当执行完后立刻释放()内对象或者类的锁。注意,只有在执行到synchronized代码块时才获取对象锁,执行完后立刻释放锁。对象调用加锁的方法也是一样的原理。另外,在synchronized代码块里面也可以继续获取其他对象的锁。
总结:
A. 无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。
B. 每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。
C. 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。
线程相关的方法Wait/notify/notifyAll(都是Object的方法)
public final void wait() throws InterruptedException:该方法只能在synchronized代码快里面调用,线程会终止并释放调用该方法的对象的锁,进入等待状态,只有调用该方法的对象调用notify方法过后被唤醒,进入就绪状态。
public final native void wait(long timeout) throws InterruptedException:该方法和wait()很相似,只是他进入等待状态过后,有个最大等待时间,如果没有超过最大等待时间,那么和wait()一模一样,但是如果超过等待时间,他不用notify唤醒,也会自动醒过来。
public final native void notify():唤醒由对象调用wait()方法等待的线程中的任意一个线程。
就是一个对象在很多线程中调用wait()方法,该对象调用notify方法就是唤醒他们中的某一个线程。
public final native void notifyAll():唤醒所有的处于等待状态的线程。