【昕宝爸爸小模块】浅谈之创建线程的几种方式

在这里插入图片描述

➡️博客首页       https://blog.csdn.net/Java_Yangxiaoyuan


       欢迎优秀的你点赞、️收藏、加❤️关注哦。


       本文章CSDN首发,欢迎转载,要注明出处哦!


       先感谢优秀的你能认真的看完本文,有问题欢迎评论区交流,都会认真回复!


浅谈之创建线程的几种方式

  • 一、✅典型解析
    • 1.1✅Runnable和Callable区别
    • 1.2✅Future
    • 1.3✅FutureTask和Callable示例
  • 二、✅拓展知识仓
    • 2.1✅Runnable接口是什么
    • 2.2✅线程安全有哪些特性
    • 2.3✅什么是原子性
    • 2.4✅如何保证多线程同时操作数据不相互污染
    • 2.5✅Thread类有哪些构造函数
    • 2.6✅原子性和并行操作有什么关系
    • 2.7✅如何保证原子性操作不被干扰
    • 2.8✅线程安全和锁有啥区别
    • 2.9✅线程安全的优缺点是什么


一、✅典型解析


在Java中,共有四种方式可以创建线程,分别是:


  • 继承Thread类创建线程
  • 实现Runnable接门创建线程
  • 通过CallableFutureTask 创建线程
  • 通过线程池创建线程




    其实,归根结底最终就两种,一个是继承Thread类,一个是实现 Runnable接口 ,至于其他的。也是基于这两个方式实现的。但是有的时候面试官更关注的是实际写代码过程中,有几种方式可以实现所以一般说4种也没啥毛病。

1.1✅Runnable和Callable区别


Runnable接口Callable接口 都可以用来创建新线程,实现Runnable的时候,需要实现run方法;实现Callable接口的话,需要实现call方法


Runnable的run方法无返回值,Callable的call方法有返回值,类型为Object。


Callable中可以够抛出checked exception,而Runnable不可以。


CallableRunnable都可以应用于executors 。而 Thread类 只支持Runnable


1.2✅Future


Future是一个接口,代表了一个异步执行的结果。接口中的方法用来检查执行是否完成、等待完成和得到执行的结果。当执行完成后,只能通过get()方法得到结果,get方法会阻塞直到结果准备好了。如果想取消,那么调用cancel()方法


FutureTaskFuture接口的一个实现,它实现了一个可以提交给Executor执行的任务,并且可以用来检查任务的执行状态和获取任务的执行结果。


1.3✅FutureTask和Callable示例


import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
* @author xinbaobaba
* FutureTask和Callable示例
*/
public class FutureAndCallableExample {
	
	public static void main(Stringl] args) throws InterruptedException, ExecutionException {
		Callable<String> callable = () -> {
			System.out.println("Entered Callable");
			Thread.sleep(2000);
			return "Hello from Callable";
		});
	
		FutureTask<String> futureTask = new FutureTask<>(callable);
		Thread thread = new Thread(futureTask);
		thread.start();


		System.out.println("Do something else while callable is getting executed");
		System.out.println("Retrieved: " + futureTask.get());
	}
}

二、✅拓展知识仓


2.1✅Runnable接口是什么


Runnable接口是Java中用于定义线程任务的接口。在Java中,可以通过实现Runnable接口来定义线程任务,实现Runnable接口的类必须实现run()方法,该方法定义了线程要执行的具体操作。通过创建Runnable对象并将其传递给Thread类的构造函数,可以创建一个新的线程,并启动该线程执行任务。


一个简单的示例:

/**
* @author xinbaobaba
* 使用Java实现Runnable接口来创建线程
*/

public class MyRunnableTask implements Runnable {
    @Override
    public void run() {
        // 线程执行的代码
        System.out.println("Hello from the thread!");
    }
}

public class Main {
    public static void main(String[] args) {
        // 创建Runnable对象
        MyRunnableTask task = new MyRunnableTask();
        
        // 创建Thread对象,并传递Runnable对象作为参数
        Thread thread = new Thread(task);
        
        // 启动线程
        thread.start();
    }
}

代码解析:定义了一个名为MyRunnableTask的类,它实现了Runnable接口并重写了run()方法。在run()方法中,我们简单地打印了一条消息。然后,在main()方法中,我们创建了MyRunnableTask的实例,并使用该实例创建了一个新的线程对象。最后,我们调用start()方法启动了线程。当线程启动后,它将自动执行我们在run()方法中定义的代码。


2.2✅线程安全有哪些特性


线程安全是多线程编程时的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。


线程安全的特性主要有以下几个方面


  1. 原子性:一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。原子性可以确保多个线程之间的操作不会相互干扰,从而保证数据的一致性。
  2. 可见性:一个线程对主内存的修改可以及时的被其他线程所观察到。可见性可以保证多个线程之间共享数据的实时更新,确保每个线程都能获取到最新的数据状态。
  3. 有序性:一个线程观察其他线程的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序。有序性可以保证代码的执行顺序按照程序员的预期进行,避免出现意外的结果。

以上就是线程安全的三大特性:原子性、可见性和有序性。这些特性可以确保多线程程序在并发执行时能够正确地处理共享数据,避免出现数据不一致、数据污染等问题

以下是一个简单的示例:



/**
* @author xinbaobaba
* 线程安全的基本特性ceshidemo
*/
public class ThreadSafeCounter {
    private int count = 0;

    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

public class CounterThread extends Thread {
    private ThreadSafeCounter counter;

    public CounterThread(ThreadSafeCounter counter) {
        this.counter = counter;
    }

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            counter.increment();
        }
    }
}

public class Main {
    public static void main(String[] args) {
        ThreadSafeCounter counter = new ThreadSafeCounter();
        CounterThread thread1 = new CounterThread(counter);
        CounterThread thread2 = new CounterThread(counter);
        thread1.start();
        thread2.start();
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Final count: " + counter.getCount());
    }
}

代码解析:定义了一个ThreadSafeCounter类,它有一个共享的count变量,用于计数。我们还定义了一个CounterThread类,它继承了Thread类,用于创建新的线程。每个线程都会执行1000次increment()操作,即增加计数。在main()方法中,我们创建了两个CounterThread线程,并启动它们。最后,我们等待两个线程执行完毕,并打印出最终的计数结果。


示例演示了线程安全的基本特性:原子性、可见性和有序性。由于increment()操作是原子的,即不会被其他线程打断,因此最终的计数结果是正确的。由于count变量的可见性得到了保证,每个线程都能观察到最新的计数结果。此外,由于线程调度和指令重排序的原因,最终的计数结果可能会与预期的顺序不同,但最终的结果仍然是正确的。


2.3✅什么是原子性


原子性是指在计算机科学中,一个操作(或一组操作)不可被中断地执行完毕或不执行,具有原子性的操作不会受到其他并发操作的干扰,能够保证数据的一致性和正确性。通俗来说,原子性就是“一气呵成”,不可分割的意思。原子性确保多个操作是一个不可以分割的整体,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行。在数据库事务中,原子性是非常重要的特性之一,它可以确保事务中的所有操作都被完整地执行或者都不被执行,从而保持数据的一致性。


2.4✅如何保证多线程同时操作数据不相互污染


要保证多线程同时操作数据不相互污染,可以采用以下几种方法


1. 使用锁机制:可以使用Java中的synchronized关键字或者Lock对象来实现对共享数据的同步访问,确保一次只有一个线程能够访问共享数据,避免多个线程同时操作数据导致数据污染。


2. 使用读写锁:读写锁允许多个线程同时读取共享数据,但在写入数据时只允许一个线程访问,这样可以提高并发性能。


3. 使用volatile关键字:volatile关键字可以确保共享数据的可见性,当一个线程修改了共享数据后,其他线程可以立即看到修改后的数据。


4. 使用事务:数据库事务可以确保一系列操作要么全部成功,要么全部失败回滚,从而避免数据的不一致性。在Java中,可以使用JDBC或者ORM框架(如Hibernate)来管理数据库事务。


5. 使用线程安全的数据结构:Java提供了很多线程安全的数据结构,如Vector、Hashtable、CopyOnWriteArrayList等,这些数据结构内部已经实现了同步机制,可以保证多个线程同时访问时的正确性。


6. 避免共享状态:尽可能地减少共享状态,让每个线程都有自己的数据副本,这样可以避免多个线程之间的数据竞争和相互污染。


7. 使用消息队列:通过消息队列可以将多个线程之间的通信解耦,每个线程将需要操作的数据发送到队列中,另一个线程从队列中获取数据进行处理,这样可以避免直接访问共享数据。


为了保证多线程同时操作数据不相互污染,可以采用多种方法来保证数据的原子性、可见性和有序性。在具体实现时,需要根据实际情况选择适合的方法来保证数据的正确性和一致性


代码示例:


/**
* @author xinbaobaba
* 如何使用synchronized关键字来保证多线程同时操作数据不相互污染
*/
public class SharedData {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

public class ThreadA extends Thread {
    private final SharedData sharedData;

    public ThreadA(SharedData sharedData) {
        this.sharedData = sharedData;
    }

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            sharedData.increment();
        }
    }
}

public class ThreadB extends Thread {
    private final SharedData sharedData;

    public ThreadB(SharedData sharedData) {
        this.sharedData = sharedData;
    }

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println(sharedData.getCount());
        }
    }
}

public class Main {
    public static void main(String[] args) {
        SharedData sharedData = new SharedData();
        ThreadA threadA = new ThreadA(sharedData);
        ThreadB threadB = new ThreadB(sharedData);
        threadA.start();
        threadB.start();
        try {
            threadA.join();
            threadB.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

代码解析:定义了一个SharedData类,它有一个共享的count变量,用于计数。我们还定义了两个线程类ThreadAThreadB,它们分别执行增加计数和获取计数的操作。在SharedData类中,我们使用synchronized关键字对increment()getCount()方法进行了同步,确保一次只有一个线程能够访问共享数据。在main()方法中,我们创建了两个线程对象,并启动它们。由于使用了同步机制,即使两个线程同时访问共享数据,也不会出现数据污染的问题。最终的计数结果将正确地累加并输出。


2.5✅Thread类有哪些构造函数


在Java中,Thread类有几个构造函数,它们允许你创建和初始化线程。以下是Thread类的构造函数:


  1. Thread():这是一个默认构造函数,用于创建一个新的线程对象,但不会自动启动线程。
  2. Thread(Runnable target):这个构造函数接受一个实现了Runnable接口的对象作为参数,该对象定义了线程要执行的代码。
  3. Thread(ThreadGroup group, Runnable target):这个构造函数接受一个ThreadGroup对象和一个实现了Runnable接口的对象作为参数。ThreadGroup对象定义了线程所属的线程组。
  4. Thread(String name):这个构造函数接受一个字符串作为参数,用于为线程设置一个名称。
  5. Thread(ThreadGroup group, String name):这个构造函数接受一个ThreadGroup对象和一个字符串作为参数。ThreadGroup对象定义了线程所属的线程组,字符串用于为线程设置一个名称。
  6. Thread(Runnable target, String name):这个构造函数接受一个实现了Runnable接口的对象和一个字符串作为参数。字符串用于为线程设置一个名称,而Runnable对象定义了线程要执行的代码。
  7. Thread(ThreadGroup group, Runnable target, String name):这个构造函数接受一个ThreadGroup对象、一个实现了Runnable接口的对象和一个字符串作为参数。这些参数允许你设置线程的名称、定义线程要执行的代码以及设置线程所属的线程组。

注意:当你使用这些构造函数创建线程时,它们只是创建了线程对象,并不会自动启动线程。要启动线程,你需要调用线程对象的start()方法。


2.6✅原子性和并行操作有什么关系


原子性和并行操作是计算机科学中的两个重要概念,它们之间存在一定的关系。


原子性是指一个操作或者一组操作不可分割地执行,要么全部执行成功,要么全部执行失败。原子性主要应用于多线程编程和数据库事务管理中,用于保证多个线程或者事务中的操作的一致性和正确性。在多线程编程中,原子性可以避免多个线程同时访问共享数据时出现数据竞争和不一致的问题。在数据库事务管理中,原子性可以保证一系列操作要么全部成功,要么全部失败回滚,从而保持数据的一致性。


并行操作是指多个操作同时执行,以提高程序的执行效率。并行操作通常应用于多核处理器和分布式计算中,通过将多个任务分配给多个处理器或者计算机节点,同时执行这些任务,可以加快程序的执行速度。


原子性和并行操作之间存在一定的关系。在并行操作中,为了保持数据的一致性和正确性,需要确保每个操作都是原子的,即不可分割地执行。同时,原子性也可以帮助避免并行操作中可能出现的数据竞争和不一致问题。在实现并行操作时,可以通过加锁、事务管理等方式来保证操作的原子性,从而保证数据的一致性和正确性。


注意:原子性和并行操作之间存在一定的矛盾和权衡。过于强调原子性可能会影响并行操作的效率,而过于强调并行操作可能会增加数据竞争和不一致的风险。因此,在实际应用中,需要根据具体场景和需求来平衡原子性和并行操作的关系,以实现最佳的性能和正确性。


代码的示例:


/**
* @author xinbaobaba
* 原子性和并行操作之间的关系示例
*/
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicityAndParallelism {
    public static void main(String[] args) {
        AtomicInteger atomicCounter = new AtomicInteger(0);
        int numThreads = 10;
        Thread[] threads = new Thread[numThreads];

        // 创建并启动多个线程,每个线程递增计数器
        for (int i = 0; i < numThreads; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    atomicCounter.incrementAndGet(); // 原子性操作
                }
            });
            threads[i].start();
        }

        // 等待所有线程执行完毕
        for (Thread thread : threads) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 输出最终的计数器值,应该等于线程数量乘以每个线程的递增值
        System.out.println("Final counter value: " + atomicCounter.get());
    }
}

代码解析:使用了AtomicInteger类来创建一个原子性计数器。AtomicInteger类中的incrementAndGet()方法是一个原子性操作,它可以保证在多线程环境下对计数器的递增操作是原子的,即不可分割地执行。这样可以避免多个线程同时访问计数器时出现数据竞争和不一致的问题。


我们创建了10个线程,每个线程都执行1000次计数器的递增操作。通过使用原子性计数器,我们可以确保最终的计数器值是正确的,即等于线程数量乘以每个线程的递增值。如果没有原子性保证,可能会出现数据竞争和不一致的问题,导致最终的计数器值不正确。


示例说明了原子性和并行操作之间的关系。在并行操作中,原子性可以保证多个操作的一致性和正确性,避免数据竞争和不一致的问题。同时,通过并行操作可以提高程序的执行效率。在实际应用中,需要根据具体场景和需求来平衡原子性和并行操作的关系,以实现最佳的性能和正确性。


2.7✅如何保证原子性操作不被干扰


要保证原子性操作不被干扰,可以采用以下几种方法:


  1. 使用锁机制:Java中的synchronized关键字或者Lock接口可以用来控制对共享资源的访问,确保一次只有一个线程能够执行某个代码块或方法,从而防止其他线程的干扰。
  2. 使用并发容器:Java提供了很多并发容器,如ConcurrentHashMap、CopyOnWriteArrayList等,这些容器内部已经实现了线程安全,可以避免多线程环境下的数据不一致问题。
  3. 使用事务:在数据库操作中,可以使用事务来保证原子性。事务是一系列的操作,要么全部成功,要么全部失败回滚,这样可以避免数据的不一致。
  4. 使用乐观锁或悲观锁:在并发控制中,乐观锁和悲观锁也是常用的手段。乐观锁基于数据版本记录机制,通过版本号或时间戳等方式来控制并发操作时的数据一致性;悲观锁则是通过数据库的行级锁或表级锁来限制并发操作。
  5. 避免共享状态:尽可能地减少共享状态,每个线程处理自己的数据副本,不与其他线程共享数据。这样自然就不存在数据不一致的问题。
  6. 使用无锁数据结构:无锁数据结构(Lock-free data structures)是另一种解决方案,它使用原子操作来更新数据,而不需要显式的锁机制。
  7. 避免长时间持有锁:尽量减少持有锁的时间,以减少其他线程的等待时间,提高并发性能。
  8. 使用分布式系统解决方案:对于大规模的分布式系统,可以考虑使用分布式事务、两阶段提交、三阶段提交等解决方案来保证操作的原子性。

总之,要保证原子性操作不被干扰,需要结合具体的应用场景和需求,选择合适的并发控制策略和工具。


2.8✅线程安全和锁有啥区别


线程安全是多线程编程中使用的概念,用于保证多个线程之间数据的安全性和一致性。线程安全涉及到多个线程对共享数据的访问和修改,需要采取措施来避免数据竞争和不一致的问题。而锁是实现线程安全的一种机制,它可以控制对共享资源的访问,确保一次只有一个线程能够执行某个代码块或方法,从而防止其他线程的干扰。锁机制包括synchronized关键字、Lock接口、乐观锁和悲观锁等。因此,线程安全是一个更广泛的概念,而锁是实现线程安全的一种具体手段。


2.9✅线程安全的优缺点是什么


线程安全的优点主要包括:


  1. 保证数据完整性:线程安全可以保证多线程环境下的数据完整性和一致性,避免数据竞争和不一致的问题。
  2. 提高系统稳定性:线程安全有助于提高系统的稳定性和可靠性,减少因数据错误或不一致导致的问题。
  3. 充分利用多核资源:线程安全可以利用多核处理器的资源,通过并行计算提高程序的执行效率。

线程安全缺点


  1. 性能开销:线程安全的实现需要引入额外的机制来控制线程的访问和同步,这可能导致一定的性能开销。
  2. 编程难度增加:线程安全的实现需要更全面深入地考虑多线程的同步和协调问题,增加了编程的复杂度和难度。
  3. 死锁和活锁风险:线程安全需要合理地设计锁机制和控制线程的执行顺序,否则可能导致死锁或活锁的问题。
  4. 资源竞争:在多线程环境下,资源竞争可能导致线程阻塞或等待,影响程序的执行效率和响应性。

因此,在实现线程安全时需要权衡利弊,根据具体的应用场景和需求选择合适的线程安全策略,以实现最佳的性能和正确性。


你可能感兴趣的:(#,Java并发,java,开发语言,线程安全,并发,并行)