进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
进程是操作系统分配资源的最小单元,线程是操作系统调度的最小单元。
一个程序至少有一个进程,一个进程至少有一个线程。
线程和进程一样分为五个阶段:新建、就绪、运行、阻塞、终止。
线程状态转换:
新建状态(New):新创建了一个线程对象
就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。
运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
1.等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
2.同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,JVM会把该线程放入锁池中。
3.其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)
死亡状态(Dead):线程执行完了或者因异常退出了run()方法或执行完了main()方法,该线程结束生命周期。
start()方法和run()方法的区别:
只有调用了start()方法,才会表现出多线程的特性,不同线程的run()方法里面的代码交替执行。如果只是调用run()方法,那么代码还是同步执行的,必须等待一个线程的run()方法里面的代码全部执行完毕之后,另外一个线程才可以执行其run()方法里面的代码。
1)继承Thread类
2)实现Runnable接口
package stu.crayue.thread;
/**
* @author Crayue
* @version 2019年11月30日 下午7:37:24
*/
public class FirstThread implements Runnable{
private String name;
public FirstThread(String name) { //根据name获得该线程
this.name=name;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(name + "运行 : " + i);
try {
Thread.sleep((int) Math.random() * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
new Thread(new FirstThread("C")).start();
new Thread(new FirstThread("D")).start();
}
}
FirstThread 类通过实现Runnable接口,使得该类有了多线程类的特征。所有的多线程代码都在run方法里面。Thread类实际上也是实现了Runnable接口的类。
在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread对象的start()方法来运行多线程代码。
实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。
synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
synchronized用于资源被用户并发访问时避免出现脏数据。
package stu.crayue.thread;
/**
* @author Crayue
* @version 2019年11月29日 下午5:43:55
*/
public class Dirtyread {
private String userName="yhy";
private String password="987220415";
public synchronized void updateUser(String userName, String password) {
this.userName = userName;
try {
Thread.currentThread().sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.password = password;
System.out.println("更新用户信息姓名为:" + userName + "密码为:" + password);
}
public /*synchronized*/ void queryUser() {
System.out.println("获取用户信息姓名:" + userName + "密码:" + password);
}
public static void main(String[] args) {
final Dirtyread dirtyread = new Dirtyread();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
dirtyread.updateUser("crayue", "12345678");
}
});
t1.start();
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
dirtyread.queryUser();
}
}
当queryUser()方法没有用synchronized加锁,dirtyread 对象获取用户名密码就和设置的数据不一样了。因为执行queryUser()这个线程还没有完成,dirtyread 对象就已经获取用户名密码。
解决方法就是给queryUser()方法加上 synchronized,就可以保证一个线程操作完数据后,其他的数据才能访问,就会避免脏数据的情况,得到结果为:
通过代码块中的锁对象,可以使用任意的对象,但是必须保证多个线程使用的锁对象是同一个
把要同步访问的代码块锁住,只让一个线程在synchronized的代码块中执行,同步中的线程没有执行完毕不会释放锁,其他线程阻塞。
public void method3(SomeObject so){
synchronized(so)
{
//…..
}
}
此时,锁就是so这个对象,谁拿到这个锁谁就可以运行它所控制的那段代码。当有一个明确的对象作为锁时,就可以这样写程序。但当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的instance变量(它得是一个对象)来充当锁。
使用synchronized修饰的方法,保证线程执行该 方法的时候,其他线程只能在外面等待。run()中调用同步方法。Synchronized修饰非静态方法,实际上是对调用该方法的对象加锁,俗称“对象锁”。锁的是this 即是这个对象。
Public synchronized void methodAAA(){
//….
}
就相当于:
public void methodAAA(){
synchronized (this) //this指的就是调用这个方法的对象
{
//…..
}
}
Synchronized修饰静态方法,实际上是对该类对象加锁,俗称“类锁”。
因为对静态对象加锁实际上对类(.class)加锁,类对象只有一个,可以理解为任何时候都只有一个空间,里面有N个房间,一把锁,因此房间(同步方法)之间一定是互斥的。
Class StaticMethod{
public synchronized static void methodAAA() // 同步的static 函数
{
//….
}
public void methodBBB(){
synchronized(StaticMethod.class) //
}
}
volatile通常被比喻成"轻量级的synchronized",也是Java并发编程中比较重要的一个关键字。和synchronized不同,volatile是一个变量修饰符,只能用来修饰变量。无法修饰方法及代码块等。
特性:
1 保证了不同线程对该变量操作的内存可见性;
2 禁止指令重排序。
volatile的使用:单例模式的实现,典型的双重检查锁定;详情请看下一章笔记。