首先,我们引出进程的概念:
1.进程:
任何一个程序在运行起来之后就是进程,生命周期为程序开始到程序结束。
每个进程在运行周期内,有自己一个PCB、虚拟地址空间、页表、以及页表映射的物理内存空间。PCB中存放着进程的数据 上下文、进程状态、PID、优先级、内存指针。由操作系统通过调度PCB来管理进程,操作系统对PCB的管理就是对进程的管理。
2.线程:
上面我们从操作系统角度浅谈了一下进程的概念;
我们有很多时候希望可以同时推进多个任务,但是,我们看到每个进程在创建时有许多的东西被创建。
因此,太麻烦,我们这个时候引出了线程的概念,在一个程序里同时执行多个任务;
创建一个进程,然后再这一个进程里创建多个任务流,一个任务流就称之为一个线程;因此,线程一套就是共享数据,因此线程就相比进程显得更加轻便简单。
在一个进程里最少都有一个线程,因此线程叫做轻量级进程。
如何创建线程呢?
大体上有三种:
1.继承Thread类;
2.实现Runnable接口;
3.实现Callable接口。
1.继承Thread类:
Java中java.lang.Thread这个类表示线程,一个类可以继承Thread类并覆写其run方法来实现一个线程;
public class HelloThread extends Thread{
public void run(){
System.out.println("Hello");
}
}
run方法是固定的public权限,没有参数,没有返回值,不抛出受查异常。
类似于单线程中的main方法,线程run方法的第一条语句开始执行直到结束。
public static void main(String[] args){
Thread thread = new HelloThread();
thread.start();
}
上述代码为:在main方法中创建了一个线程对象,并调用其start方法,当调用start方法后,HelloThread中的run方法开始执行。
start表示启动线程,使其成为一条单独的执行流,操作系统会分配线程相关的资源,每个线程会有单独的程序计数器和栈(后面会单独将栈跟程序计数器),操作系统会将此线程作为一个独立的个体进行调度, 执行的起点就是run方法。
当直接在main方法中调用run方法的话,只是单纯的调用了一个方法,并没有体现线程的概念,这个时候可以使用currentThread()来查看当前线程。
2.实现Runnable接口
通过继承Thread类实现线程比较简单,但是Java里面只支持单继承,如果已经有了父类,就不可以在继承Thread类,于是就有了Runnable接口。
Runnable接口里只有一个run方法。
public class HelloRunnable implements Runnable{
public void run(){
System.out.println("hello");
}
}
想要启动线程,还要创建一个Thread对象,在传递一个Runnable对象:
public static void main(String[] args){
Thread helloThread = new Thread(new HelloRunnable());
helloThrea.start();
}
还可以使用匿名内部类和Lambda表达式来实现接口:
1.匿名内部类:
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
});
thread.start();
}
2.Lambda表达式:
public static void main(String[] args) {
Runnable runnable = ()-> System.out.println("hello");
new Thread(runnable).start();
}
使用Runnable接口可以避免单继承局限以及更好的体现程序共享的概念。
3.实现Callable接口
Runnable接口中run方法没有返回值,但有些情况需要有返回值,此时就需要实现Callable接口。该接口中没有run方法,就只有一个call方法。
public interface Callable {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
call方法的意义跟run方法一样,里面是线程实现的主体代码:
class MyThread implements Callable {
private int ticket = 10 ; // 一共10张票
public String call() throws Exception {
while(this.ticket>0){
System.out.println("剩余票数:"+this.ticket -- );
}
return "票卖完了,下次吧。。。" ;
}
}
然后再调用start方法来启动线程,但是Thread类中不可以接受Callable类型的参数;
但是有一个类FutureTask可接收Callable类型的参数;
然后FutureTask实现了RunnableFuture接口;
RunnableFuture接口继承了Runnable接口;
同样,Thread类也继承了Runnable接口。
public static void main(String[] args) {
FutureTask futureTask = new FutureTask(new Mythread);
new Thread(futureTask).start();
}
这样就启动了覆写了Callable接口的线程。
4.线程有一些基本属性及方法。
属性:id、名称、优先级、状态
方法:sleep方法、yield方法、join方法、stop方法、interrupt方法、
每一个线程都有id、名称,id是一个递增的整数,每创建一个线程就加一。名称的默认值是Thread-后加编号,也可以自己指定,在Thread构造方法中,也可通过setName方法来设置。
优先级,可能会优先执行的线程,在程序中设置优先级可能会映射到操作系统中。
设置优先级:public final void setPriority(int newPriority)
取得优先级:public final int getPriority()
状态:表示当前线程的状态。
1.public static native void sleep(long millis) throws InterruptedException;
调用该方法会让当前线程睡眠指定的时间,单位是毫秒:
睡眠期间,该线程会让出CPU,但是不会释放对象锁。
2.public static native void yield();
yield方法会让出CPU让其他线程先执行,但是不会释放对象锁;yield方法会让相同优先级的线程获得CPU的时间,yield方法是将线程从运行状态调整至准备状态,并不是变为阻塞状态。
3.public final void join() throws InterruptedException;
一个线程等待另一个线程的退出,然后再往下执行,在等待的过程中,这个等待可能被中断,如果被中断,会抛出异常。
4. public final void stop();
stop方法会解除线程获取的所有锁定,当一个线程对象调用stop方法时候,该对象所运行的线程会立即停止。
因为stop方法会造成线程不安全的情况,因此,已经被标记为过时方法。
5. public void interrupt();
interrupt只是对线程进行中断,中断并不是强迫终止一个线程,它给线程传递一个取消信号,由线程来决定如何以及何时退出。
a.对于正在执行的线程,interrupt方法只是设置线程的中断标志位,没有任何其他的作用。
b.当线程调用join/wait/sleep方法后,线程对象调用interrupt方法会抛出异常。抛出异常后,中断标志位会被清空,而不是被设置:
public static void main(String[] args) {
Thread t = new Thread(){
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println(isInterrupted());
}
}
};
t.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
t.interrupt();
}
上述的输出为false。
c.如果线程正在等待锁,调用interrupt方法只会设置线程的中断标志位,线程依旧处于等待的状态:
public class Main {
private static Object lock = new Object();
private static class A extends Thread{
@Override
public void run() {
synchronized (lock){
while (!Thread.currentThread().isInterrupted()){
System.out.println("哈哈哈");
}
}
System.out.println("exit");
}
}
public static void test() throws InterruptedException {
synchronized (lock){
A a = new A();
a.start();
Thread.sleep(1000);
a.interrupt();
a.join();
}
}
public static void main(String[] args) throws InterruptedException {
test();
}
}
上面的代码,主线程持有了lock锁,然后启动线程a,然而线程a也尝试获得lock锁,所以就进入等待锁队列,随后调用interrupt方法和join方法,来等待a线程结束。
然而运行的结果是线程并没有结束。
5.守护线程
守护线程是一种特殊线程,线程分为两种,“用户线程”,“守护线程”。
只要当前JVM进程中存在任何一个非守护线程没有结束,守护线程就在工作;只有当最后 一个非守护线程结束时,守护线程才会随着JVM一同停止工作。
将当前线程标记为守护线程或用户线程:public final void setDaemon(boolean on),on为true为守护线程
判断守护线程:public final boolean isDaemon(),返回值为true为守护线程。
垃圾回收机制就是典型的守护线程。
6.synchronized与同步:
public class Main implements Runnable {
private static int counter = 0;
@Override
public void run() {
for (int i = 0;i<1000;i++){
counter++;
}
}
public static void main(String[] args) throws InterruptedException {
int num=1000;
Thread [] threads = new Thread[num];
for(int i=0;i
上述代码创造了1000个线程,每个线程对counter循环加了1000次,主线程会等待所有线程结束后打印counter的值,期望的结果是100万,然而打印出来却是90多万。因为上述代码的counter++操作不是原子性操作。
counter++分为三个步骤:
1.取得counter的值;
2.在当前基础上加1;
3.将新值赋给counter。
两个线程可能同时执行第一步,然后重复写入相同的值。
因为上述的情况就需要引入锁的概念。
synchronized是为了防止多个线程同时执行同个对象的同步代码块;
synchronized可用于修饰类的实例方法、静态方法和代码块;
1.实例方法:
class Counter{
private int counter = 0;
public synchronized void increaseNum(){
counter++;
}
public synchronized int getCounter(){
return counter;
}
}
public class Main implements Runnable {
Counter counter;
public Main(Counter counter) {
this.counter = counter;
}
@Override
public void run() {
for (int i = 0;i<1000;i++){
counter.increaseNum();
}
}
public static void main(String[] args) throws InterruptedException {
int num=1000;
Counter counter = new Counter();
Thread [] threads = new Thread[num];
for(int i=0;i
将代码中加入synchronized关键字来修饰,这样方法内的代码就变成了原子操作,这次无论怎样结果都为100万;
在上述代码中传入的对象只有一个counter,是同一个对象;
因此,synchronized实例方法实际保护的是同一个对象的方法调用,确保只能有一个线程来执行。
synchronized保护的是对象而非代码,只要访问同一个对象的synchronized方法,即使不同的代码,也会同步顺序访问。
2.静态方法
class Counter{
private static int counter = 0;
public static synchronized void increaseNum(){
counter++;
}
public static synchronized int getCounter(){
return counter;
}
}
public class Main implements Runnable {
@Override
public void run() {
for (int i = 0;i<1000;i++){
Counter.increaseNum();
}
}
public static void main(String[] args) throws InterruptedException {
int num=1000;
Thread [] threads = new Thread[num];
for(int i=0;i
上述实例方法中保护的是实例对象是this,对于静态方法保护的是类对象。
3.代码块
class Counter{
private int counter = 0;
public void increaseNum(){
synchronized (this){
counter++;
}
}
public int getCounter(){
synchronized (this){
return counter;
}
}
}
public class Main implements Runnable {
Counter counter;
public Main(Counter counter) {
this.counter = counter;
}
@Override
public void run() {
for (int i = 0;i<1000;i++){
counter.increaseNum();
}
}
public static void main(String[] args) throws InterruptedException {
int num=1000;
Counter counter = new Counter();
Thread [] threads = new Thread[num];
for(int i=0;i
synchronized的特征:
1.可重入性;
2.内存可见性。
可重入性:对同一个执行线程,它获得了锁之后,在调用其它需要同样锁的代码的时候,就可以直接调用;
内存可见性:在释放锁的时候,所有写入都会写回内存,而获得锁后,都会从内存中读取最新的数据;而若是只需要保证内存可见性,可用volatile修饰变量。
死锁:
使用synchronized或者其他锁,应注意死锁;
public class Main {
private static Object locka = new Object();
private static Object lockb = new Object();
private static void startThreadA(){
Thread athread = new Thread(new Runnable() {
@Override
public void run() {
synchronized (locka){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
synchronized (lockb){
}
}
}
});
athread.start();
}
private static void startThreadB(){
Thread bthread = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lockb){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
synchronized (locka){
}
}
}
});
bthread.start();
}
public static void main(String[] args) throws InterruptedException {
startThreadA();
startThreadB();
}
}
这样athread与bthread陷入了互相等待的过程。
以上就是我对线程的一些浅薄的理解,好了,不说了,继续敲代码了