进程与线程
一个程序至少有一个进程,一个进程至少有一个线程。
线程的划分尺度小于进程,使得多线程程序的并发性高。
进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。
在Java中,每次程序运行至少启动2个线程:
1.main主线程
2.垃圾收集线程
两种主要的并发情况
IO流阻塞:数据读写,是可以调剂资源的;可以尽可能的拆分线程。
运算阻塞:一次不可以中断的阻塞,是系统无法在运算结束之前调剂资源的阻塞方式;最多阻塞的数量相当于CPU的内核数。要尽可能的避免出现运算阻塞这种情况
线程的创建
线程创建的2种方式
- 继承Thread:要通过重写或者实现run方法来完成线程的主体功能
public class ThreadCreateTest {
public static void main(String[] args) {
Thread t = new InheritThread();
t.start();
}
}
class InheritThread extends Thread {
@Override
public void run() {
System.out.print("继承Thread创建了线程");
}
}
运行结果:
继承Thread创建了线程
- 实现Runnable:要通过重写或者实现run方法来完成线程的主体功能
public class ThreadCreateTest {
public static void main(String[] args) {
Thread t = new Thread(new RunnableIpl());
t.start();
}
}
class RunnableIpl implements Runnable{
@Override
public void run() {
System.out.println("实现Runnable类创建了线程");
}
}
执行结果:
实现Runnable类创建了线程
其他说明
1.每一个线程都有一个属于自己独立的执行序列
2.main方法属于主线程;而自己创建的属于子线程
3.线程还可以分为用户线程和守护线程:用户线程是必须执行完的线程;当用户线程执行完以后,无论守护线程是否执行完,都立即停止
4.线程的启动时通过start()方法来启动,而不是通过调用run()方法来启动!
5.继承Thread创建线程时,不会强制要求重写run方法,实现Runnable时则必须重写run方法
6.每个线程都有一个调用栈,每创建一个新的线程,就产生一个新的调用栈
设置线程名称、获取当前线程
设置线程名称:t.setName(String name)
获取当前线程:Thread t = Thread.currentThread()
public class ThreadCreateTest {
public static void main(String[] args) {
Thread t = new Thread(new RunnableIpl());
t.setName("金盏花");
t.start();
}
}
class RunnableIpl implements Runnable{
@Override
public void run() {
System.out.println("实现Runnable类创建了线程");
Thread t = Thread.currentThread();
System.out.println(t.getName());
}
}
执行结果
实现Runnable类创建了线程
金盏花
注:线程默认名称:Thread-0 …… n
线程栈
public class ThreadStack {
public static void main(String[] args) {
System.out.print("此处主线程执行");
test();
}
static void test() {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.print("此处子线程执行");
}
});
t.start();
}
}
说明:
程序运行ThreadStack的main方法时,系统分配线程栈1给主线程,线程栈1将main压入栈中,主线程运行到test方法时,会将test压入线程栈1中,test运行时,创建了线程,系统分配桟2给test创建的线程,桟2将run方法压入栈中
等待
方法 | 说明 |
---|---|
void wait() | 导致当前的线程等待,直到其他线程调用此对象的 notify()方法或 notifyAll()方法。 |
void notify() | 唤醒在此对象监视器上等待的单个线程。 |
void notifyAll() | 唤醒在此对象监视器上等待的所有线程。 |
注意:
1.通过对象来进行调度
2.必须在同步状态下才可以等待
等待的生命周期
a.创建实例
b.准备就绪
c.运行(即start)时,会获取锁,持有锁之后,等待系统分配资源,获取资源运行相关内容,运行途中因为某些原因,线程进入等待(wait)时,会释放锁
d.线程等待时间到/被唤醒时,线程需要再次获取锁,然后进入就绪状态,获得资源执行后面的内容,执行完成后,会释放锁
休眠
1)任何地方都可以休眠
2)调用Thread的sleep方法进行休眠
3)休眠不可唤醒,只可以时间到自己醒
4)休眠如果在同步状态下,是不会释放锁的
生命周期:
a.创建实例
b.准备就绪
c.运行----->休眠---->时间到---->就绪状态---->运行
优先级
不要用!不靠谱,不推荐,不指望!
1)线程的优先级在1-10直接
2)10最高,1最低
3)不靠谱,不能指望
4)让步是让当前正在运行的线程,直接回到可运行状态,重新抢资源
线程的合并
1)Join:让一个线程加入到另一个线程之后,顺序执行
2)先start再join
3)join类似于sleep,在前一个线程执行之后执行;调用了join以后,依然会回到可执行状态
4)可以指定时间,前面一个线程如果执行时间过长,那么合并线程指定时间到了以后立即执行。
public class ThreadStack {
public static void main(String[] args) throws Exception{
System.out.println("此处主线程执行");
Thread t1 = new ThreadTest();
t1.start();
t1.join();
Thread t2 = new ThreadTest();
t2.start();
t2.join();
Thread t3 = new ThreadTest();
t3.start();
t3.join(1000);
Thread t4 = new ThreadTest();
t4.start();
t4.join();
Thread t5 = new ThreadTest();
t5.start();
t5.join();
}
}
class ThreadTest extends Thread{
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName());
Thread.sleep(3000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
执行结果:
此处主线程执行
Thread-0
Thread-1
Thread-2
Thread-3
Thread-4
线程的生命周期
一旦线程启动,它就永远不能再重新启动。只有一个新的线程可以被启动,并且只能一次。
线程生命周期如下:
1.创建线程实例;
2.调用start方法,让线程变为可执行状;
3.等待系统分配资源
4.有资源之后,进入运行状态
5.run方法执行结束,中途没有任何阻塞情况(线程被调度),那么该线程生命周期结束
同步与锁
概念:线程同步指的是,即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作。
多线程访问同一个资源会产生线程安全问题,所以需要进行加锁。
每个锁在同一时刻,只能由一个线程持有。
方法或声明执行期间,如程序遇到任何异常或return,线程都会释放锁。
1.多个线程未加锁
public class LockObject {
public static void main(String[] args) {
for(int i = 0; i < 10000; i++) {
new Thread(new ThreadTest1()).start();
}
}
}
class ThreadTest1 implements Runnable{
@Override
public void run() {
MathOpera.getMathOpera().add();
}
}
class MathOpera{
public MathOpera() {}
private static MathOpera m = new MathOpera();
public static MathOpera getMathOpera() {
return m;
}
int num = 0;
public void add() {
num++;
System.out.println("增加后的值:" + num);
}
}
按照平时理解的逻辑,for(int i = 0; i < 10000; i++) {…}执行了10000次,num最后应该是10000,但实际上num可能不是10000,因为有些线程可能获取到了同样的num,将num加1后的值赋值过去,相当于执行了至少2次同样的值加1
…
增加后的值:9986
增加后的值:9987
增加后的值:9988
增加后的值:9989
增加后的值:9990
增加后的值:9991
增加后的值:9992
增加后的值:9993
增加后的值:9994
增加后的值:9995
增加后的值:9996
synchronized:在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中,都是锁对象
锁在方法上:
class MathOpera{
public MathOpera() {}
private static MathOpera m = new MathOpera();
public static MathOpera getMathOpera() {
return m;
}
int num = 0;
public synchronized void add() {
num++;
System.out.println("增加后的值:" + num);
}
}
执行结果:
…
增加后的值:9993
增加后的值:9994
增加后的值:9995
增加后的值:9996
增加后的值:9997
增加后的值:9998
增加后的值:9999
增加后的值:10000
锁代码块:
class MathOpera{
public MathOpera() {}
private static MathOpera m = new MathOpera();
public static MathOpera getMathOpera() {
return m;
}
int num = 0;
public void add() {
synchronized(this) {
num++;
}
System.out.println("增加后的值:" + num);
}
}
执行后结果:
…
增加后的值:9994
增加后的值:9995
增加后的值:9996
增加后的值:9997
增加后的值:9998
增加后的值:9999
增加后的值:10000
一般而言,锁方法比较霸道,方法被锁了之后,其他线程都只能等待当前线程释放锁之后才能执行,但方法中有部分内容是不需要安全控制的,相当于上厕所时,有人将厕所的大门给关了,其他人进不了厕所,但是你只用到厕所的一个隔间,其他你用不到,锁代码块就相当于锁隔间,其他隔间其他人去使用。一般是找有共享数据修改的代码块来上锁。
实例方法可以通过锁对象控制线程,但是静态方法是不根据对象存在,是在类里的,如果静态方法需要枷锁,可以通过锁类来达到目的
静态方法add里加入对象的同步锁时,可以看到,编译错误:
锁类之后:
public class LockClass {
public static void main(String[] args) {
for(int i = 0; i < 1000; i++) {
new Thread(new ThreadTest2()).start();
}
}
}
class ThreadTest2 implements Runnable{
@Override
public void run() {
new MathOperate().add();
}
}
class MathOperate{
static int addCount = 0;
public static void add() {
synchronized(MathOperate.class) {
addCount++;
System.out.println("增加后的值:" + addCount);
}
}
}
执行结果:
…
增加后的值:988
增加后的值:989
增加后的值:990
增加后的值:991
增加后的值:992
增加后的值:993
增加后的值:994
增加后的值:995
增加后的值:996
增加后的值:997
增加后的值:998
增加后的值:999
增加后的值:1000